In one of my previous articles about compile-time computations we have seen how you can ‘abuse’ the new keyword constexpr
in order to achieve some interesting effects. Now it is time to show some of the intended usages of the keyword. I could probably write about creating more compile-time constants, but you probably know it already, for instance from this proposal.
This article is about constant initialization. In short, this is a new way global, static and thread-local objects (not necessarily constant) can be initialized without running into problems of initialization order or a data race.
Troubles with globals
In this post, by ‘globals’ I mean a couple of categories of objects:
- namespace-scope objects with static storage duration (the namespace can be global or anonymous),
- namespace-scope objects with thread storage duration,
- static data members of classes,
- temporaries bound to ‘global’ references.
The usage of globals in C++ is problematic in many ways. They often make the programs difficult to reason about because they break the locality of the code. Whenever you call any function you do not know if it modified the global or not. Accessing globals is likely to introduce data races unless you remember to use atomic globals or protect your globals with some locking mechanism. Since C++ has no notion of ‘dynamic libraries’ it is unspecified how the globals behave when a program is dynamically linked to a library: should either have a copy of a global? Should globals be initialized before libraries are linked? But you cannot initialize globals in a dynamic library before you link with it… Next, globals can be accessed and used before they have been created or after they have been destroyed.
I only want to focus on one problem: the initialization order of globals. Consider this example spread across two files:
file 1:
// declaration extern std::mutex m; std::thread t1{job1}; std::thread t2{job2}; //... |
file 2:
// definition std::mutex m; // ... |
The situation with mutex m
is fairly common in many programs: the declaration is made visible through a header file, and the global object is defined in a separate file. Then, before main
starts we launch two threads that execute two jobs. Parts of the jobs may be executed before main
starts The two threads will have to synchronize on the mutex m
. Synchronizing on the mutex requires checking if it is locked (reading its value) and locking it (writing the value). But by the time the first of the threads attempts to lock the mutex for the first time, do you think the mutex will have been initialized with the default constructor?
If you are aware of the problem called “static
initialization order fiasco,” you probably already sensed the problem: m
is a global in one translation unit (file), t1
and t2
are globals defined in another translation unit, the order of initialization between globals in different translation units is usually indeterminate…
I emphasized word ‘usually’ because in C++11, much more than in C++03, this is not always the problem, and in fact, the above example is free from initialization order fiasco problem owing to the feature called static initialization.
Zero-initialization
The mechanism in general can be described as follows: if the constructor of your type meets certain constraints, and arguments passed at construction meet certain constraints, your global is guaranteed to be initialized before the program is even run: at compile-time. This mechanism isn’t entirely new and used to be available in C++03 in limited form of zero-initialization.
Globals of types with trivial default constructor — like int
s, float
s, pointers or aggregates thereof — without any explicit initializer, are always initialized with zeros at compile-time. This is easily achieved, as globals reside at global addresses, known at compile-time (although this is not the case for thread-local objects), and it doesn’t cost the compiler a lot to fill this memory with zeros. This is different for automatic objects which are allocated at run-time, and zeroing them out would incur some run-time overhead.
Zero-initialization of globals is sufficient for a number of useful designs. For instance, we can make a counter by declaring a global int
without any initializer:
int counter; // no initializer
And we are guaranteed that it is initially set to zero. Similarly, we can implement a lazy load using a raw pointer like this:
Utility * ptr; // no initializer Utility & getUtility() { if (!ptr) { ptr = new Utility; } return *ptr; }
And again, we are guaranteed that we start with a null-pointer value. However, (concurrency issues aside) we have a problem with the last example: someone needs to destroy the lazily loaded object. You could think about using a smart pointer, but a smart pointer is not a ‘primitive’ type and most likely has a user-provided default constructor (or no default constructor). The zero-initialization guarantee is lost.
So, you could think about the approach to initialization encouraged in language Go: just arrange data in your type so that zero-initialization puts your object in a valid and ready to use state. For pointer it is easy:
template <class T> class SmartPointer { T * rawPtr_; // no constructor };
Zero initialization will put your object into a well-understood state: a null smart pointer storing a null raw pointer. In the case of mutex, this approach constraints the implementation. This is how an implementation for mutex could look like:
class mutex { bool youCanLockMe_; vector<thread::id> threadsLocked_; // invariant: if (youCanLockMe_) threadsLocked_.empty(); };
Here, youCanLockMe_
indicates that no thread has locked the mutex, and threadsLocked_
contains the list of threads that are currently locked and need to be notified if the current owner has released the lock. This implementation will not work with zero-initialization for two reasons. Flag youCanLockMe_
will be zero-initialized to false
which will mean that the mutex is initially locked and there would be no-one to unlock it. Second, vector
cannot be used when zero-initialized: its default constructor (only called at run-time) still needs to be called before any attempt to access the vector: otherwise we would get a UB.
On the other hand, the following implementation would work:
class treadIdLink { thread::id id_; SmartPointer<treadIdLink> next_; }; class mutex { bool locked_; SmartPointer<treadIdLink> threadsLocked_; // invariant: if (!locked_) threadsLocked_ == nullptr; };
Now we ‘inverted’ the meaning of the flag, so now the zero-initialized flag indicates a ready-to-lock mutex. We also store the list of locked threads in a linked list. This way, we can represent an empty list with a null-pointer: the result of zero-initializing a smart pointer (hopefully).
While the above approach is acceptable for a mutex (because it does not need any other constructor), it will not in fact work for our SmartPointer
because a smart pointer really needs other constructors: e.g., one that takes a raw pointer. Then, if we provide a constructor, compiler requires of us to provide a default constructor, and if we do, using our pointer before its (run-time) default constructor is called would be a UB. But even if we defined our smart pointer without any constructor it still (as well as our mutex
) has another serious problem: zero-initialization will only work for globals; creating automatic objects or dynamically creating objects would result in garbage initial value (as it is the case for basic types like int
s, float
s, pointers or aggregates thereof).
Constant initialization
C++11 extends the idea of zero-initialization so that any initialization of globals that is sufficiently simple is guaranteed to be performed at compile-time. You can already guess that ‘sufficiently simple’ is defined by the same rules that govern constexpr
functions and constructors. This is probably nothing new to you that constexpr
functions can be used to evaluate and create constants of complex types at compile-time, but here we are talking about initializing objects that are not const at all: a mutex is meant to change its state in time.
So how do C++11 rules work? After all globals have been zero-initialized, constant initialization takes place: every global object whose initialization involves only the access to compile-time constants and evaluation of constant expressions has its initial value set at compile-time, before any run-time initialization of globals takes place. Thus initialized object does not itself have to be a compile-time constant (and if so, its value cannot be used in constant initialization of other globals).
How do we use it? With almost no effort:
template <class T> class SmartPointer { T * rawPtr_; constexpr SmartPointer() noexcept : rawPtr_{nullptr} {}; // other constructors };
This declaration may look a bit awkward at first but lets inspect it in detail. constexpr
says that this constructor is a constexpr
-constructor; i.e., it could be used to create compile-time constants if our class were a literal type (but our pointer is not one) or it can be executed during the static-initialization phase, provided that any expression involved is a constant expression — and this property does not require that our type be a literal type. noexcept
is not essential for constant initialization, but since we are defining a very simple function (so simple that it can be initialized at compile-time with constant expressions) it is worth also signalling that this is a no-fail function and could be used as a building block in implementing strong and basic exception safety guarantees. Our constructor is not trivial: a trivial constructor would not initialize the rawPtr_
to null-pointer value, so we initialize it explicitly. Setting a raw pointer to null-pointer value is a constant expression, so in turn, our constructor is a simple enough type to guarantee static initialization.
What does this buy us? Now if a global object of type SmartPointer<T>
is initialized with a default constructor, the language guarantees that the initialization is performed at compile time, so that we will never run onto the order-of-initialization issue. At the same time other constructors may require dynamic initialization. We create the global the same way we would in C++03:
// at global namespace SmartPointer<int> intPtr;
We do not need to (and in fact should not) type any constexpr
. It is enough that the default constructor is declared as constexpr
constructor. intPtr
is not a compile-time constant. It is not even a ‘run-time constant’ we ca change its value at will at run-time. It is only the initialization that is guaranteed to be performed at compile-time. Similarly, we can guarantee that the mutex is const-initialized:
class mutex { bool locked_; SmartPointer<treadIdLink> threadsLocked_; // invariant: if (!locked_) threadsLocked_ == nullptr; constexpr mutex() noexcept : locked_{false}, threadsLocked_{nullptr} {} };
We do not have to initialize our members with zero values, so the following implementation is also possible:
class mutex { bool youCanLockMe_; SmartPointer<treadIdLink> threadsLocked_; // invariant: if (youCanLockMe_) threadsLocked_ == nullptr; constexpr mutex() noexcept : youCanLockMe_{true}, threadsLocked_{nullptr} {} };
However, we cannot go back to using std::vector
for storing locked thread IDs because vector’s default constructor is not (required to be) a constexpr
constructor.
Constant initialization gotchas
So we have a tool that enables us to perform some classes of globals’ initialization at compile-time. But there are some essential difficulties that we should pay attention to when relying on constant initialization.
It is difficult to assert that the initialization of globals really took place at compile-time. You can inspect the binary, but it only gives you the guarantee for this binary and is not a guarantee for the program, in case you target for multiple platforms, or use various compilation modes (like debug and retail). The compiler may not help you with that. There is no way (no syntax) to require a verification by the compiler that a given global is const-initialized. If you were initializing a constant of a literal type you could ask the compiler to check that:
constexpr double x{1.0}; double y{2.0}; // oops! y not a compile-time const const Complex c1{x, y}; // constant initialized at run-time constexpr Complex c2{x, y}; // error: y not a compile-time const
The second use of constexpr
tells the compiler to verify that all expressions used to initialize c2
are constant expressions (and ‘y
’ is not); but apart from constant initialization we also requested the immutability of the object. If we only require constant initialization alone we cannot use constexpr
specifier, but then if we inadvertently use even one sub-expression that is not a constant expression (and some expressions, like type conversions, are not even visible) we silently move the initialization to run-time. Therefore the best way to avoid such potential problems is to rely with the constant initialization only on default constructors which cannot inadvertently take ‘non-constant-expression’ arguments.
Another gotcha lies in the fact that in case of constexpr
function/constructor templates, if a given instantiation of a function fails to meet all the requirements imposed on the constexpr
function, compiler is allowed (and in fact required) to silently drop the constexpr
specifier and downgrade it to an ordinary function. Therefore, it is safer if we use ‘non-template’ types like mutex
, or if it is a template parametrized by T
, not call any operations of T
(which we know nothing about) — this is the case for our SmartPtr
.
And this is more-less the guidelines adapted by the C++ Standard Library. Apart from generating compile-time constants, static initialization is only provided for default construction and construction from nullptr
for several types: std::unique_ptr
, std::shared_ptr
, std::weak_ptr
, std::mutex
, std::once_flag
. One cold also expect a similar guarantee of STL containers, however while such requirement makes sense, it was decided that it would be too much a constraint for STD library providers, who need to have some flexibility in how they want to implement the library to meet other goals, such as run-time efficiency.
An interesting exception from these guidelines is class template std::atomic
, where default constructor is not constexpr
(it leaves the value uninitialized in order to guarantee maximum run-time performance for automatic objects, compatible with C language), but value constructors are.
Summary
I have exaggerated a bit about initialization at compile-time. What the C++ Standard really guarantees is that zero-initialization (initialization with zeros) and constant initialization (initialization with constants) (the two are collectively called static initialization) are performed before any other (dynamic) initialization takes place in the program. Thus, it is possible that some time at the program start-up will be taken for static initialization. Yet, anything said about avoiding the order-of-initialization problems holds, and because compiler writers are driven not only by the ISO C++ Standard but also by the market needs (e.g., performance) it is natural to expect that such initialization should be done by the compiler; for free.
One observation that its worth repeating here is that constexpr
functions/constructors play two different roles:
- Creating and evaluating compile-time constants of fairly complex types.
- Initializing mutable global objects of fairly complex types at compile-time.
References
- “Generalized constant expressions” — a sequence of proposals for generalizing constant expressions (N1521, N1972, N1980, N2116, N2235) by Gabriel Dos Reis, Bjarne Stroustrup and Jens Maurer.
- The C++ International Standard. It used to be accessible from the Committee’s website. If you need to refer to an online resource, the latest standard draft — N3376 — is pretty similar in content. The following are the relevant places therein: § 3.6.2 (Initialization of non-local variables), § 3.8 (Object lifetime), § 5.19 (Constant expressions), § 7.1.5 (The
constexpr
specifier), § 8.5 (Initializers), § 12.1 (Constructors), § 12.9 (Inheriting constructors). - N2994: “
constexpr
in the libray: take 2” by Alisdair Meredith. It contains loads of useful information about constant initialization.
Nice article. I have a question though. Instead of writing constructors for Constant Initialization wouldn’t it be easier to use in-class member initializers? C++11 compiler has to prefix a constructor with constexpr if only possible so it could be much easier to implement than to maintain constructors code later on.
And one more thing. You probably meant “and ‘y’ is not” in “Constant initialization gotchas” section?
An interesting observation. I have not thought of that. It would definitely be a better alternative for
mutex
class. It would also work forSmartPointer
above, although I would hesitate before default-initializing a member pointer tonullptr
, because it is not that often that theSmartPointer
will be default-initialized.I am now quickly trying to think if this technique could be generalized beyond the two examples from this post. Consider class:
These two constructors are special and probably both deserve a chance to participate in constant initialization, but require to be initialized to different (and somewhat opposite) states.
If you need 2 constructors than you know that you have to provide default constructor because it will not be created for you. However you can manually declare it as constexpr and use = default to make compiler provide a body based on your in-class member initialization. The other (not default) constructor still has to be provided by hand and initialized properly.
The only downside I see with in-class member initializers od PODs is a non global usage where all members will still be somehow initialized while in ‘regular’ way their initialization would be just skipped. It may cause some performance penalty for some guys that really count every CPU cycle. However that is exactly the same case as yours with constexpr default constructors.
constexpr SmartPointer() noexcept : rawPtr_{nullptr} {};
Let me ask two questions.
1. Why is there a semicolon after ctor definition? Seems to be redundant.
2. Why noexcept? I thought constexpr implies noexcept.
@bobobobo:
constexpr
does not implynoexcept
,and in factconstexpr
functions are allowed to throw, and it may prove useful.constexpr
functions can be executed at run-time, and when it comes to compile-time, there should exist a “control path” that only involves compile-time evaluations, but it is allowed to also have control paths that require run-time evaluations: provideded that these paths are not executed at compile-time. For examples, see my other post on compile-time computations.I’m extremely glad to hear that compile time initialization has found it’s way into the good graces of C++. The only thing I don’t see in your discussions is whether linker resolved addresses are considered valid constexpr elements. As an example, I have some initializers which look like this:
typedef struct DoubleLinkedList_struct {
DoubleLinkedListNode m_anchor;
} DoubleLinkedList;
#define DoubleLinkedList_INITIALIZER(me) { {&(me)->m_anchor,&(me)->m_anchor,}, }
such that I can compile (really link) time initialize a global instance like this:
DoubleLinkedList g_list = DoubleLinkedList_INITIALIZER(&g_list);
Essentially the structure points to itself (creating an empty list). Is &g_list now considered a constexpr? Thanks for this writeup.
Hi Scott. Thanks for the interesting question. The answer, if I correctly understood your question, is yes. Quoting the Standard (§5.19 ¶3):
Thus, for instance, the following code compiles fine (in GCC 4.7):
I’ve been putting off reading this article for quite some time, and now I wish I hadn’t!
Thanks so much for clearing up a lot of my misunderstanding on the usage of constexpr on constructors and also on constant initialization.
Still, constant initialization correctly continues to seem fairly “expert friendly”
I’m curious to try out Mateusz Pusz’s suggestion of class member initializers.
My overall impression of constant initialization is that it is simply dangerous if you try to use it for constructors other than the default one (or the ones that only take tags like
nullptr_t
,boost::none_t
): because there are no means to verify if all prerequisites for constant initialization have been met.One thing that normal programmers could be taught is to identify classes where you are able to provide a simple default constructor. ‘Simple’ here would mean not requiring any resources, impossible to fail (and throw exceptions), do the minimum for the object to be able to say that it has not been assigned a proper value yet. This is the case for all smart and dumb pointers,
boost::optional
and other nullable types likestd::function
. (These classes would also provide other constructors, so the default one will not get auto-generated.)Once identified, declare it as:
In order to enable the static initialization.
The ‘default’ follows Mateusz’s suggestion to initialize members in their declaration. The ‘noexcept’ is not necessary in this exercise, but it feels logical for types with so simple initialization.
Pingback: std::optional in C++14 | Some Things Are Obvious
Just bumped into your post today, thanks for the insight (have been looking for something like this for a while). I was intrigued by your statement that you cannot verify that constant initialization has taken place and tried to prove you wrong. The end result is that it seems you indeed can’t guarantee it works, but it seems possible to at least detect some cases where constant initialization did not happen (though I’m not quite happy with the syntax for that yet).
I’ve done a full writeup on my own blog: http://www.stderr.nl/Blog/Software/Cpp/ConstantInitialization.html
Suggestions on other approaches to try are welcome 🙂
Since this was written, C++20 seems to have introduced a new keyword, `constinit` to do exactly this: Require that at variable is constant-initialized, without also making it `const` like `constexpr` does. See https://en.cppreference.com/w/cpp/language/constinit