Thursday 7th May 2015: 4.11pm. Link shared: http://cppnow2015.sched.org/event/37beb4ec955c082f70729e4f6d1a1a05#.VUuMqvkUUuU
As part of publicising my C++ Now 2015 talk next week, here is part 2 of 20 from its accompanying Handbook of Examples of Best Practice for C++ 11/14 (Boost) libraries:
2. COUPLING: Strongly consider versioning your library's namespace using inline namespaces and requesting users to alias a versioned namespace instead of using it directly
C++ 11 adds a new feature called inline namespaces which are far more powerful than they first appear:
namespace boost { namespace afio { inline namespace v1 { /* stuff */ } } }
// Stuff is generated at the ABI link layer in boost::afio::v1
// But to the compiler everything boost::afio::v1::* appears identically in boost::afio::*
// INCLUDING for ADL and overload resolution
// In other words you can declare your code in boost::afio::v1, and use it as if declared in boost::afio.
// The other important C++ feature here is namespace aliasing, so
namespace local { namespace afio = boost::afio; /* use afio::* and it all works */ }
The reason this pattern is so useful is because it greatly eases the lives of your end users and you the library maintainer in years to come when you need to break API compatibility. Let's take a case example: imagine the situation typical in 03 C++ libraries where library Boost.Foo uses dependent library Boost.AFIO:
namespace boost { namespace afio {
struct foo {
static void api(int i, double f);
};
} }
...
namespace boost { namespace foo {
boost::afio::api(1, 2);
} }
Imagine that you now release an API breaking refactor of Boost.AFIO, which would look like this:
namespace boost { namespace afio {
struct foo {
static void api(double f, int i); // Oh dear, f and i have been swapped!
};
} }
...
namespace boost { namespace foo {
boost::afio::api(1, 2); // This is probably now a bug!
} }
The users of Boost.Foo which uses boost::afio::foo::api() now finds that their library no longer passes its unit testing because foo::api() has changed from before. They will quite rightly throw up a fuss, and under Boost's present rules you will be asked to roll back your refactor until Boost.Foo has also been refactored to match your refactor. This causes inconvenience for you the maintainer of Boost.AFIO, the maintainer of Boost.Foo, and is a general pain for users. It also breaks modularity, increases coupling between libraries in a way which saddles you the maintainer of Boost.AFIO with the lack of maintenance or failure of timely maintenance of libraries dependent on AFIO, and I can't strongly enough recommend you don't blindly copy the 03 idiom of suggesting client code use your library directly using fully qualified namespacing.
The good news is we can make all this go away with inline namespaces and namespace aliasing, so consider this pattern instead:
namespace boost { namespace afio { inline namespace v1 {
struct foo {
static void api(int i, double f);
};
} } }
...
namespace boost { namespace foo {
// Probably somewhere in this library's config.hpp
namespace afio = boost::afio; // This is the key use change which needs to be strongly recommended to your library's users
...
// In implementation code after config.hpp
afio::api(1, 2); // Note the no longer fully qualified use of afio. The local namespace alias is used to "symlink" to "the latest" version of Boost.AFIO
} }
Now imagine your refactor occurs as before:
namespace boost { namespace afio {
// Probably defined by boost/afio.hpp which in turn includes boost/afio_v2.hpp
inline namespace v2 {
struct foo {
static void api(double f, int i); // new implementation
};
}
// Probably defined by boost/afio_v1.hpp
namespace v1 {
struct foo {
static void api(int i, double f); // old implementation
};
}
} }
...
namespace boost { namespace foo {
// Probably somewhere in this library's config.hpp
namespace afio = boost::afio::v1; // By changing this one single line we "fix" the problem. Earlier we included <boost/afio_v1.hpp> instead of <boost/afio.hpp>.
...
// In implementation code after config.hpp
afio::api(1, 2); // And this finds the v1 AFIO implementation, not the v2 implementation
} }
What have we just achieved?
1. Library Boost.Foo dependent on Boost.AFIO no longer requires lots of refactoring work if Boost.AFIO is refactored. Just two lines changed in its config.hpp, something easy for the release managers to do.
2. Library Boost.AFIO can now be evolved far quicker than before, and simply keep shipping entire copies of legacy versions without problems with colliding namespaces. As end users get round to upgrading, legacy versions can be removed from the distro after a period of warning.
What are the problems with this technique?
1. You now need to ship multiple copies of your library, maintain multiple copies of your library, and make sure simultaneous use of multiple library versions in the same executable doesn't conflict. I suspect this cost is worth it for the added flexibility to evolve breaking changes for most library maintainers. You probably want to employ a per-commit run of http://ispras.linuxbase.org/index.php/ABI_compliance_checker to make sure you don't accidentally break the API (or ABI where appropriate) of a specific API version of your library, so in your custom build run you might check out an original SHA for your library separate to your latest commit, build both and use the ABI compliance checker tool to determine if anything has broken. Similarly, the same toolset (ABIDump) could be used to detect where ABIs collide by having some shell script error out if any ABI overlaps between two libraries, perhaps using the diff tool.
Also don't forget that git lets you recursively submodule yourself but pinned to a different branch by adding the `submodule.name.branch` stanza to .gitmodules, so if you do ship multiple versions you can mount specific version tracking branches of yourself within yourself such that a recursive submodule update checks out all the versions of yourself into a given checkout.
2. The above technique alone is insufficient for header only end users where multiple versions of your library must coexist within the same translation unit. With some additional extra work using the preprocessor, it is possible to allow multiple header only library versions to also coexist in the same translation unit, but this is covered in a separate recommendation below.
3. Many end users are not used to locally aliasing a library namespace in order to use it, and may continue to directly qualify it using the 03 idiom. You may consider defaulting to not using an inline namespace for the version to make sure users don't end up doing this in ways which hurt themselves, but that approach has both pros and cons.
Some fun extra things this technique enables:
1. Something not so obvious above is that you can also stub out fake copies of dependencies where that dependency is missing in the current config. For example, imagine optional compression support where your config.hpp either namespace aliases to boost::foo::compression either a real compression library, or an internal stub copy which actually does nothing. Your code is then written to assume a compression library aliased at boost::foo::compression and need not consider if it's actually there or not. The advantages here for reducing coupling are very obvious.
2. This technique is highly extensible to allow dependency injection of STL11 vs Boost on a per-feature basis e.g. your user wants Boost.Thread instead of STL11 thread but only for threading, so your library can be so modular as to allow both options to end users. This is covered in a separate recommendation below.
Examples of libraries which use versioned namespaces and aliasing to bind a namespace locally:
* https://boostgsoc13.github.io/boost.afio/
#cppnow #cppnow2015 #c ++ #boostcpp #c ++11 #c ++14