In this post I’ll discuss the problem of initializing global variables with
static storage duration, and show you some of the solutions available in C++.
C++ makes some pretty sparse guarantees about the initialization of such global variables:
- Storage for them is zero-initialized.
- Initialization happens either prior to the first statement of the main function, or before the first use of the variable.
- The order of initialization is guaranteed to happen in the same order as the variable declarations appear within a single translation unit. Initialization order between translation units is not guaranteed.
That last point is what concerns us most: how can we order the initialization of
static globals to reflect dependencies in our code?
Let’s begin with an example which exhibits the behaviour we’re talking about. Imagine we have a
Log singleton which opens a file during initialization and provides a method to write to that file:
Note that we also close the file in the
Log destructor, hence deinitialization order is going to be important as well; we cannot call
Log::Log() or after
Now let’s imagine another singleton (imaginatively named
Subsystem) which attempts to call
Log::write() during initialization and deinitialization:
Obviously this relies on
Log::s_instance being initialized before
Subsystem::s_instance. If not, our calls to
Log::write() will fail because the file was not created. Unfortunately there is no guarantee that this is the case. We are entirely at the whim of the compiler;
Log::s_instance might be initialized before
Subsystem::s_instance or vice versa. We just don’t know.
With Visual Studio I was able to get this code to fail consistently by ensuring that the
Subsystem files appeared before the
Log files inside the .vcxproj.
Solution 1: Lazy Initialization
One solution is to use lazy initialization. During
Log::write() we simply check if the file was created, and create it if it wasn’t:
This is simple and it works. But what about deinitialization? The problem remains - it’s still possible that
~Log() will be called before
~Subsystem(), which means that there is no sensible place to close our log file. It also means we have to test
m_file every time we write to the log, which isn’t ideal.
Solution 2: Explicit Initialization/Deinitialization
Another idea might be to provide explicit
Shutdown() methods for
Subsystem and move the ctor/dtor code into those methods. This way we can decide exactly when and where to create and destroy the log file:
This is a complete solution, but not a particularly elegant one. It depends entirely on the client code to order the dependencies, which becomes a maintenance nightmare as the number of
Shutdown() pairs increases. Determining the order of dependencies is a hard problem for any non-trivial example, and what if the dependencies change and we forget to change the order of
Shutdown() calls? Bad bad bad.
Solution 3: Schwarz Counter
And so at last we come to the ‘Schwarz’ or ‘Nifty’ counter. The idea is to declare a new class or struct
LogInit, plus a static instance of
LogInit in the header:
This means that every translation unit which includes
Log.h will get it’s own static instance of
LogInit and, therefore,
~LogInit() will be called once for each translation unit. The initialization order is controlled by the counter
Log::s_instance on the heap for brevity, but you can imagine other solutions e.g. using aligned storage and placement new, or calling static
s_count to ensure that the ctor/dtor are called exactly once, on the first and last calls to
~LogInit() respectively. Because each translation unit gets its own
LogInit instance, whichever ‘goes first’ during the pre-main initialization will increment
s_count to 1 and subsequently call the
Log ctor. Conversely, whichever translation unit ‘goes last’ during the post-main deinitialization will decrement
s_count to 0 and call the
Log dtor. Adding new dependencies ‘just works’ - we never have to worry about getting the initialization order right since the counter handles this for us. With one exception:
Log depends on another singleton
FileSystem (to create the log file), but
FileSystem also depends on
Log (to print an error message)? The Schwarz counter idiom can’t help us here: there’s no way to order the initialization of
FileSystem which works.
The only advice in this case is to avoid cyclic dependencies like this - remove any
Log::GetInstance()->write() calls from the
FileSystem ctor/dtor, or vice versa. Dropping an
GetInstance() to check that the init happened is also useful. Hopefully these cases will be quite rare and easy to resolve by design. Note that this problem only applies to dependencies in the initialization/deinitialization - everywhere else is safe.