Book Image

Modern C++ Programming Cookbook - Third Edition

By : Marius Bancila
Book Image

Modern C++ Programming Cookbook - Third Edition

By: Marius Bancila

Overview of this book

The updated third edition of Modern C++ Programming Cookbook addresses the latest features of C++23, such as the stack library, the expected and mdspan types, span buffers, formatting library improvements, and updates to the ranges library. It also gets into more C++20 topics not previously covered, such as sync output streams and source_location. The book is organized in the form of practical recipes covering a wide range of real-world problems. It gets into the details of all the core concepts of modern C++ programming, such as functions and classes, iterators and algorithms, streams and the file system, threading and concurrency, smart pointers and move semantics, and many others. You will cover the performance aspects of programming in depth, and learning to write fast and lean code with the help of best practices. You will explore useful patterns and the implementation of many idioms, including pimpl, named parameter, attorney-client, and the factory pattern. A chapter dedicated to unit testing introduces you to three of the most widely used libraries for C++: Boost.Test, Google Test, and Catch2. By the end of this modern C++ programming book, you will be able to effectively leverage the features and techniques of C++11/14/17/20/23 programming to enhance the performance, scalability, and efficiency of your applications.
Table of Contents (15 chapters)
13
Other Books You May Enjoy
14
Index

Using inline namespaces for symbol versioning

The C++11 standard has introduced a new type of namespace called inline namespaces, which are basically a mechanism that makes declarations from a nested namespace look and act like they were part of the surrounding namespace. Inline namespaces are declared using the inline keyword in the namespace declaration (unnamed namespaces can also be inlined). This is a helpful feature for library versioning, and in this recipe, we will learn how inline namespaces can be used for versioning symbols. From this recipe, you will learn how to version your source code using inline namespaces and conditional compilation.

Getting ready

In this recipe, we will discuss namespaces and nested namespaces, templates and template specializations, and conditional compilation using preprocessor macros. Familiarity with these concepts is required in order to proceed with this recipe.

How to do it...

To provide multiple versions of a library and let the user decide what version to use, do the following:

  • Define the content of the library inside a namespace.
  • Define each version of the library or parts of it inside an inner inline namespace.
  • Use preprocessor macros and #if directives to enable a particular version of the library.

The following example shows a library that has two versions that clients can use:

namespace modernlib
{
  #ifndef LIB_VERSION_2
  inline namespace version_1
  {
    template<typename T>
    int test(T value) { return 1; }
  }
  #endif
  #ifdef LIB_VERSION_2
  inline namespace version_2
  {
    template<typename T>
    int test(T value) { return 2; }
  }
  #endif
}

How it works...

A member of an inline namespace is treated as if it were a member of the surrounding namespace. Such a member can be partially specialized, explicitly instantiated, or explicitly specialized. This is a transitive property, which means that if a namespace, A, contains an inline namespace, B, that contains an inline namespace, C, then the members of C appear as they were members of both B and A and the members of B appear as they were members of A.

To better understand why inline namespaces are helpful, let’s consider the case of developing a library that evolves over time from a first version to a second version (and further on). This library defines all its types and functions under a namespace called modernlib. In the first version, this library could look like this:

namespace modernlib
{
  template<typename T>
  int test(T value) { return 1; }
}

A client of the library can make the following call and get back the value 1:

auto x = modernlib::test(42);

However, the client might decide to specialize the template function test() as follows:

struct foo { int a; };
namespace modernlib
{
  template<>
  int test(foo value) { return value.a; }
}
auto y = modernlib::test(foo{ 42 });

In this case, the value of y is no longer 1 but 42 instead because the user-specialized function gets called.

Everything is working correctly so far, but as a library developer, you decide to create a second version of the library, yet still ship both the first and the second version and let the user control what to use with a macro. In this second version, you provide a new implementation of the test() function that no longer returns 1 but 2 instead.

To be able to provide both the first and second implementations, you put them in nested namespaces called version_1 and version_2 and conditionally compile the library using preprocessor macros:

namespace modernlib
{
  namespace version_1
  {
    template<typename T>
    int test(T value) { return 1; }
  }
  #ifndef LIB_VERSION_2
  using namespace version_1;
  #endif
  namespace version_2
  {
    template<typename T>
    int test(T value) { return 2; }
  }
  #ifdef LIB_VERSION_2
  using namespace version_2;
  #endif
}

Suddenly, the client code breaks, regardless of whether it uses the first or second version of the library. This is because the test function is now inside a nested namespace, and the specialization for foo is done in the modernlib namespace, when it should actually be done in modernlib::version_1 or modernlib::version_2. This is because the specialization of a template is required to be done in the same namespace where the template was declared.

In this case, the client needs to change the code, like this:

#define LIB_VERSION_2
#include "modernlib.h"
struct foo { int a; };
namespace modernlib
{
  namespace version_2
  {
    template<>
    int test(foo value) { return value.a; }
  }
}

This is a problem because the library leaks implementation details, and the client needs to be aware of those in order to do template specialization. These internal details are hidden with inline namespaces in the manner shown in the How to do it... section of this recipe. With that definition of the modernlib library, the client code with the specialization of the test() function in the modernlib namespace is no longer broken, because either version_1::test() or version_2::test() (depending on what version the client actually uses) acts as if it is part of the enclosing modernlib namespace when template specialization is done. The details of the implementation are now hidden to the client, who only sees the surrounding namespace, modernlib.

However, you should keep in mind that the namespace std is reserved for the standard and should never be inlined. Also, a namespace should not be defined inline if it was not inline in its first definition.

See also

  • Using unnamed namespaces instead of static globals, to explore anonymous namespaces and learn how they help
  • Chapter 4, Conditionally compiling your source code, to learn the various options for performing conditional compilation