![]() |
Arene Base
Fundamental Utilities For Safety Critical C++
|
It is often useful to declare variables constexpr, so that they are compile-time constants, with the ensuing benefits:
By default, constexpr variables have internal linkage, the same as static variables at file scope, which means that if a constexpr variable is declared in a header file, then it is actually a different variable in each translation unit that includes that header file.
Mostly, this does not cause an issue. However, if the variable is used in an inline function, or a function template, or member function of a class template that is defined in a header file, then each of the uses of the inline function or template in different translation units now refer to different instances of the constexpr variable. This is thus a violation of the One Definition Rule (ODR), and Undefined Behaviour.
C++17 introduces the concept of inline variables, which work like inline functions in that if they are duplicated in multiple translation units, then the duplicates are coalesced, and only one instance of the variable is created. The uses of such a variable by inline functions or templates in multiple translation units is thus OK, as the same instance of the variable is referenced in each case.
Given the following declarations:
the_answer is now the same variable in all translation units, as is inline_size_of_t<int> or inline_size_of_t<std::string>.
The use of inline variables thus avoids the ODR violation and avoids undefined behaviour.
C++14 does not allow the inline keyword to be applied to variables. However, the same effect can be achieved for variable templates by using the extern specifier. In this case, the compiler treats the variable template the same as a non-constexpr variable template, and ensures there is only one global instance for each set of template parameters.
extern keyword may not be required for recent versions of some compilers, as the C++ standard has been clarified to say that all variable templates are supposed to be implicitly extern, but it is always correct to specify it explicitly, and avoids portability problems.In this example, there is only one extern_size_of_t<int> instance in the program, even if used in multiple translation units.
This does not work with variables that are not templates though: using extern in such a case will lead to duplicate definition errors, since constexpr variables must be initialized at the point of declaration, and duplicate definitions of extern variables is always an error.
Having found a solution for variable templates, we can extend that to non-template variables. The first step is to define a variable template that has exactly one instantiation, by using a Non-Type Template Parameter of type std::nullptr_t, which has exactly one possible value: nullptr.
The default parameter means that we can use the_answer_impl<> to refer to the one and only instantiation of that variable template in all translation units.
The trailing <> still provides a suboptimal experience though, so we need to eliminate that. We can do this by providing a reference to the variable template, with the desired name.
This should be declared static to explicitly mark it with internal linkage, to ensure and highlight that there is actually a distinct reference in each translation unit. This is explicitly what we were trying to avoid in the initial case, but this is now what we want: without it, we will get duplicate definition errors. The key difference here is that this is a reference, and each use of this reference refers to the same instance of the_answer_impl<>. This means that all uses of the_answer in all translation units refer to the same variable, so cannot cause ODR violations.
This is used in multiple places in Arene Base. e.g. arene::base::swap is defined using this pattern: