foonathan::blog()

Thoughts from a C++ library developer.

Inline Namespaces 101

Almost three years ago — wow, how time flies — I blogged about namespace aliases and called them one of C++ most underrated features (which probably was a bit of a click bait).

Let’s talk about some other namespace feature, that is, well, not quite underrated, but relatively obscure: inline namespace. They are namespaces that don’t really introduce a scope, except when they do.

So what can you do with them?

What Are inline Namespaces?

C++11 introduced inline namespaces. They are namespaces that aren’t really namespaces: Everything declared inside of them are also part of the parent namespace.

namespace foo // normal namespace
{
    void foo_func(); // function inside normal namespace
}

inline namespace bar // inline namespace
{
    void bar_func(); // function inside inline namespace
}

foo::foo_func(); // okay
bar::bar_func(); // also okay

foo_func(); // error, no such function
bar_func(); // okay, inline namespace!

This seems … pointless?

But there are two use cases for this feature.

API Versioning

Suppose you have written a library with some utility class foo:

namespace my_library
{
    class foo
    {
        
    };
}

But you’re not quite happy with foo, so in a future version, you’ve improved it substantially. Sadly, the new foo is not completely backwards compatible: Some users have to use the old version.

So to ease the transition, you still provide both:

namespace my_library
{
    namespace v1
    {
        // old foo
        class foo {  };
    }
    inline namespace v2
    {
        // new, improved foo
        class foo { … };
    }

    // note: no `foo` in `my_library` directly
}

Most users just continue to use my_library::foo and will silently get the v2 version. Users that can’t use v2 just need to switch to my_library::v1::foo instead. This can be a transition that is a lot easier to do.

// on update it will get the shiny new v2 version
my_library::foo f;

// but maybe I don't want it, just change the namespace
my_library::v1::foo f;

But why do we need inline namespaces for that? Can’t we just do this?

namespace my_library
{
    namespace v1
    {
        // old foo
        class foo {  };
    }
    namespace v2
    {
        // new, improved foo
        class foo { … };
    }

    using namespace v2;
}

That way my_library::foo will work as well and resolve to v2.

While that’s true, v2::foo still isn’t part of my_library. This has implications for ADL (it will not look inside my_library), template specializations etc.

Guideline: When doing a breaking API change, consider adding a nested inline v2 namespace and putting the new API there, while the old one is in a nested v1 namespace. Then users who need to keep the old API just need to manually opt-in as required.

ABI Versioning

The other use case is for ABI versioning. If you don’t know what ABI is, consider yourself lucky!

Once upon a time, people wrote C libraries and shipped them to the world. Users could just write their own programs, link to those libraries and use them. If an update for the library is available and the library hasn’t changed its API, there was no need to recompile your project, just relink it to the new version (or do nothing in case of dynamically linked libraries): The linker will resolve all calls of the library to the new definitions.

Then C++ came and everything changed.

You see, while the way C functions are compiled—the ABI—is pretty much standardized for an OS, this isn’t the case with C++. Relinking with a new version is only guaranteed to work if the new version is built with the exact same compiler and flags.

Furthermore, API changes in C almost had a 1:1 correspondence to ABI changes: stuff like adding parameters to a function or data members to a struct are observable API changes. Not so with C++: you can do a lot of API compatible changes that change how the program is compiled. For example, adding a private data member to a class is an ABI breaking change, but the API isn’t modified at all!

This created a somewhat volatile environment, where you have to be careful to ensure that the ABI isn’t changed. If you changed it, the calling code and the called code might not agree on the way data is laid out in memory, which creates really weird bugs!

Consider a library with header and implementation:

// library.h
namespace my_library
{
    class foo
    {
        int i = 42;

    public:
        void do_sth() const;
    };
}

// library.cpp
#include <iostream>

#include "library.h"

void my_library::foo::do_sth() const
{
    std::cout << i << '\n';
}

When we’re calling it from an executable it prints out 42, as expected:

// application.cpp
#include "library.h"

int main()
{
    my_library::foo f;
    f.do_sth();
}

But consider what happens when the library changes to:

// library.h
namespace my_library
{
    class foo
    {
        float f = 3.14; // new!
        int i = 42;

    public:
        void do_sth() const;
    };
}

// library.cpp
#include <iostream>

#include "library.h"

void my_library::foo::do_sth() const
{
    std::cout << i << '\n';
}

Recompiling the library and relinking it, but not recompiling the application and we get something like 1059720704 (it’s UB)! Application and library no longer agree on the layout of foo. sizeof(foo) for the application is still sizeof(int), and it doesn’t know about the float member. But inside do_sth() there is a float member, so it access uninitialized memory after the space reserved by the application.

Life Hack: Just recompile whenever you get a new dependency version. It makes your life so much better.

This is where inline namespaces help. While an inline namespace is completely transparent on the C++ side, it isn’t transparent on the assembly level. The mangled name of functions — the translated version used to make overloading possible — does contain the inline namespace.

So we put foo into an inline namespace:

// library.h
namespace my_library
{
    inline namespace abi_v1
    {
        class foo
        {
            int i = 42;

        public:
            void do_sth() const;
        };
    }
}

Our application program will write my_libray::foo but actually use my_library::abi_v1::foo. And likewise the call will go to my_library::abi_v1::foo::do_sth().

When we add the float, we switch to abi_v2. Then on relinking there is a linker error, as there is no my_library::abi_v1::foo::do_sth() anymore! You have to recompile, so you call the abi_v2 version instead.

That way, the ABI mismatch is detected instead of materializing as mysterious UB.

Guideline: As a library author, consider adding an inline ABI version namespace that is updated on every ABI breaking change (or all the time). That way users have to recompile in order to link against the new version.

Note that the ABI version doesn’t need to be in any way correlated to the API version. It can just be an integer you bump every time you do an ABI breaking change or want users to recompile.

As ABI breaking changes are often necessary to implement certain optimizations, most libraries shouldn’t provide ABI stability. It just makes it a lot harder for a library implementer. As such, the ABI version should be bumped a lot.

Also note that for header-only libraries you don’t need to worry about it at all: users can’t relink it anyway.

Conclusion

inline namespaces can be a useful tool.

If you care about API backwards compatibility, you can provide older versions of the API in parallel to newer ones, completely transparent to the regular user.

And if you change an inline namespace with every ABI breaking change (or release, if you don’t want ABI compatibility), you can prevent mysterious bugs when users just relink to the library without actually recompiling their program.

Finally, if you don’t like the nested namespaces, don’t worry: With C++20 you can write namespace my_library::inline v1 {, which is a nice improvement of C++17 nested namespace declaration.

This post was made possible by my Patreon supporters. If you'd like to support me as well, please head over to my Patreon and do so! One dollar per month can make all the difference.