Version 5 (modified by 7 years ago) ( diff ) | ,
---|
Links To Examples Of Best Practice For C++ 11/14 Libraries
originally written by Niall Douglas May 2015
As part of preparing my C++ Now 2015 presentation "A review of C++ 11/14 only Boost libraries" I examined ten C++ 11/14 mandatory libraries heading towards Boost which were:
Name | Authors | Min C++ | Boost headers required | Entered Boost peer review queue | Description | Repository |
---|---|---|---|---|---|---|
Boost.Fiber | Oliver Kowalke | 14 | Context, Config | Conditionally accepted | a framework for micro-/userland-threads (fibers) scheduled cooperatively | https://github.com/olk/boost-fiber |
Boost.AFIO | Niall Douglas, Paul Kirth | 11 | none | 2013-10 | strongly ordered portable asynchronous filesystem and file i/o extending ASIO | https://github.com/BoostGSoC13/boost.afio |
Boost.DI | Krzysztof Jusiak | 14 | none | 2015-01 | provides compile time, macro free constructor dependency injection | https://github.com/krzysztof-jusiak/di |
Boost.Hana | Louis Dionne | 14 | none | 2015-04 | functional programming in C++ based around transformations of heterogeneous collections of types. | https://github.com/ldionne/hana |
Boost.Http | Vinícius dos Santos Oliveira | 11 | asio, filesystem, system, datetime, utility | 2015-04 | create applications that need to expose services through HTTP | https://github.com/BoostGSoC14/boost.http |
Boost.APIBind | Niall Douglas | 11 | none | not submitted yet | toolkit for modularizing Boost libraries, managing versioned API and ABI dependency binds, makes it easy for Boost libraries to switch between use of the C++ 11 STL and the Boost STL | https://github.com/ned14/Boost.BindLib |
Boost.Expected | Pierre Talbot, Vicente J. Botet Escriba | 11 | none | not submitted yet | category and functional programming in C++ based around the monadic expected<T, E> | https://github.com/ptal/expected |
Boost.Tick | Paul Fultz II | 11 | none | not submitted yet | trait introspection and concept creator for C++11 | https://github.com/pfultz2/Tick |
Boost.Fit | Paul Fultz II | 11 | none | not submitted yet | provides utilities for functions and function objects | https://github.com/pfultz2/Fit |
Boost.Sqlpp11 | Roland Bock | 11 | none | not submitted yet | A type safe embedded domain specific language for SQL queries and results in C++ | https://github.com/rbock/sqlpp11 |
There was very significant divergence in best practices between these libraries at the time of examination, and moreover the divergences were uneven possibly reflecting an uneven understanding of what best practice might be in C++ 11/14. Thence from my examination I have prepared the list below of what I felt were best practices in C++ 11/14 mandatory libraries with hyperlinks to the relevant source code files in the relevant libraries above. As what a "best practice" is or is not may vary according to opinion, I have attempted to provide a rationale for each of the suggestions below. It is up to library authors to weigh each suggestion and to decide whether to apply it, or not, to their library.
One will note that the list below is much more C++ 11/14 focused than Boost focused. This is because it is derived from the first crop of C++ 11/14 mandatory Boost libraries. This is not a handbook for writing Boost libraries or even C++ 11/14 Boost libraries, if you want that first start reading here (note some of the guidelines on that page don't really apply to C++ 11/14 libraries) and then read here and here.
I have tried to keep these points generic to all C++ 11/14 libraries in the hope that they are useful far outside Boost. I have also ordered them with what I consider the most important ("strongly consider") first and not as important ("consider") later.
-
Links To Examples Of Best Practice For C++ 11/14 Libraries
- 1. Strongly consider using git and GitHub to …
- 2. Very strongly consider versioning your library's namespace using …
- 3. Strong consider trying your library on Microsoft Visual Studio 2015
- 4. Consider making it possible to use an XML outputting unit testing …
- 5. Consider not doing compiler feature detection yourself
1. Strongly consider using git and GitHub to host a copy of your library and its documentation
All the Boost libraries are on github, and all the free tooling exampled below integrates easily with github. Choosing github makes your life much easier. Note that as git is a distributed source code control system, you can keep a canonical master copy anywhere and write a script which autorefreshes the github copy, thus triggering any of the free tooling you have integrated there.
Github also provides free website hosting for HTML. Have a script automatically generate documentation and commit it to the gh-pages branch in your normal repo. This should present a copy of your HTML at http:// username.github.io/repository.
This is the script which generates the documentation for proposed Boost.AFIO, and indeed you can see the exact output generated by this script at http://boostgsoc13.github.io/boost.afio/. You may find it useful.
cd boost-local rm -rf libs/afio/doc/doxy/doxygen_output/html mkdir -p libs/afio/doc/doxy/doxygen_output/html cd doc ../b2 -a ../libs/afio/doc cd ../.. if [ ! -e publish ]; then git clone -b gh-pages git@github.com:BoostGSoC13/boost.afio.git publish fi cd publish git reset --hard b1414e11be50ff81124e2e1583f1bbb734ad9ead cd .. rm -rf publish/* mkdir -p publish/doc/html cp -a boost-local/doc/html/afio* publish/doc/html/ cp -a doc/src publish/doc/ cp -a doc/src/images/boost.png publish/ cp -af boost-local/doc/src publish/doc/ mkdir -p publish/libs/afio/doc cp -a doc/* publish/libs/afio/doc/ cd boost-local/libs/afio/doc/doxy doxygen cd ../../../../../publish cp -a ../boost-local/libs/afio/doc/doxy/doxygen_output/html . cp -a ../Readme.md . cp -a ../Readme.md Readme.html echo '<html><head><title>Boost.AFIO documentation</title><meta http-equiv="refresh" content="300"/><body> <h1>Boost.AFIO documentation</h1> <p><a href="doc/html/afio.html">BoostBook format documentation</a></p> <p><a href="html/index.html">Doxygen format documentation</a></p> <p><a href="afio-stable.tar.bz2">Ready to go stable AFIO distribution with all git submodules (from master branch)</a></p> <p></p>' > index.html cat Readme.md | tail -n +4 >> index.html echo ' </body></html>' >> index.html git add . git commit -a -m 'SHA stamp by Jenkins as building correctly' || true git push -f
Some may wonder what the hard git reset to a SHA is for. This prevents the gh-pages branch continuously growing in storage by breaking history for the branch, and therefore making git clone times grow excessively. As the branch is purely for HTML publishing, breaking history like this is safe.
Other examples of libraries which use github for their documentation:
- https://olk.github.io/libs/fiber/doc/html/
- https://boostgsoc13.github.io/boost.afio/
- https://krzysztof-jusiak.github.io/di/boost/libs/di/doc/html/
- https://boostgsoc14.github.io/boost.http/
2. Very 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 new C++ 11 feature 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?
- 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.
- 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?
- 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 for most library maintainers.
- 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, 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.
- 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:
- 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.
- 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.
3. Strong consider trying your library on Microsoft Visual Studio 2015
More than half the libraries reviewed had no support for Microsoft Visual Studio, and only supported GCC and clang. When the authors were asked why, in many cases it was simply assumed that MSVC didn't implement much C++ 11/14 and the authors hadn't attempted MSVC support.
This is in fact untrue. Here is a complete list of C++ 11/14 features which VS2015 does NOT support (everything else you can assume it supports, including a full and complete C++ 14 STL):
- Expression SFINAE (there are workarounds. Note the STL "turns on" magic Expression SFINAE support for those parts of the STL requiring it, so any Expression SFINAE done by the STL for you works as expected).
- Any serious constexpr use (try "#define constexpr" i.e. disable it completely. Most of the time your code will compile and work. Consider using a BOOST_CONSTEXPR macro thereafter). It is claimed by Microsoft that full C++ 11 constexpr conformance is present, but to be honest in my own code I don't find anything less than C++ 14 constexpr useful in practice.
- No two phase lookup. Reordering decls to make the compiler look up in a way which doesn't produce non-conforming outcomes is a straightforward, if annoying, workaround.
- MSVC's C99 support is still less than complete, but it's significantly more complete than before.
- MSVC's preprocessor is still non-conforming, but it's less broken than it has ever been.
- Variable templates.
- Non-static data member initialisers for aggregates.
VS2015 is a very highly conforming C++ 11/14 compiler. It meets or exceeds clang 3.3 on every C++ feature, so if your library can be compiled by clang 3.3 then it is highly likely it should compile, without too much work, on VS2015. VS2015 even has some support for C++ 1z (C++ 17) matching about what clang 3.5 provides with the main showstoppers being lack of relaxed constexpr and no variable templates. See http://blogs.msdn.com/b/vcblog/archive/2015/04/29/c-11-14-17-features-in-vs-2015-rc.aspx.
I am not claiming that you won't get surprises when you try getting MSVC to compile your code which you thought was standards compliant. MSVC is not an AST based compiler and uses heuristics to trigger partial AST compilation, and therefore has a unique processing model which exposes your assumptions about what you think or thought is valid C++. This is exactly why it is worthwhile getting your C++ 11/14 library working on MSVC because you will get a better quality, more standards conforming C++ library out of it.
4. Consider making it possible to use an XML outputting unit testing framework, even if not enabled by default
A very noticeable trend in C++ 11/14 mandatory Boost libraries is that less than half are able to use Boost.Test, and use alternative unit testing systems.
There are many very good reasons not to use Boost.Test, but there are few good reasons to not be able to use Boost.Test at all.
TODO IN PROGRESS