What is the Static Initialization Order Fiasco in C++ ?
In this article, I'll be covering a subtle but egregious problem that can occur in C++ programs. This problem is popularly called the 'Static Initialization Order Fiasco'. I'll first go over what the problem is, then go onto some solutions and explore how they work. Let's get started.
What is the 'Static Initialization Order Fiasco' in C++ ?
The C++ standard states the order in which static objects are initialized across different translation units is undefined or ambiguous. A translation unit is just a way of saying a file that is fed into the compiler - it's a C++ source file with all the code from the headers included in it. One thing to note though, for later in the article: static objects in the same translation unit are constructed in order of declaration and destructed in the reverse order.
So, how is this a problem ?
It can be a problem in the following situation :
Let's say there are 2 static objects in 2 different files. File1.cpp
has a static object of type class A - aObj
. File2.cpp
has a static object of type class B - bObj
. The static object in File1.cpp
is visible to File2.cpp
since it declares aObj
as extern
in File1.h
.
// Static initialization order problem
// File1.h
class A {
....
void doSomething() {
...
}
}
extern A aObj;
//File1.cpp
static A aObj;
// File2.cpp
class B {
B() {
aObj.doSomething();// Not okay! aObj may have already been destructed!
}
....
}
static B bObj;
In this program, it is possible that the object aObj
in File1.cpp gets initialized before bObj
in File2.cpp. That is all good since in that case, the constructor for bObj
runs after aObj
has been constructed. It is safe to call call methods on aObj
.
However, it is also possible that the object bObj
in File2.cpp gets initialized before aObj
in File1.cpp. In that case, the constructor of bObj
calls doSomething()
on aObj
which has not been constructed! The memory has been allocated for aObj
, but it hasn't been constructed. This could lead to unintended behavior / corrupt program.
So this was the static initialization order fiasco. The other problem is the static de-initialization order fiasco! This is pretty much the same problem, just applied to the order of de-initialization of static objects. The C++ standard doesn't specify the order in which static objects get de-initialized as well. So it is possible that static object aObj
gets destroyed before bObj
. This is a problem if bObj
's destructor uses or references aObj
. This is illustrated in the code snippet below - it is pretty much the same as the example above, just that its the de-initialization order which is dangerous this time:
// Static de-initialization order problem
// File1.h
class A {
....
void doSomething() {
...
}
}
extern A aObj;
//File1.cpp
static A aObj;
// File2.cpp
class B {
B() {}
~B() {
aObj.doSomething(); // Not okay! aObj may have already been destructed!
}
....
}
static B bObj;
Note: These problems are only applicable to objects with static storage scope. They wouldn't occur if bObj
was a variable with automatic storage scope. In that case, the C++ standard guarantees that aObj
is constructed before bObj
and destructed after it.
Note: These problems also do not occur in C programs. Why is that so? Since in C, there's no concept of constructors and destructors. Static objects are completely defined during compile time.
Now that it is clear what the problem is, I will discuss some solutions. There are multiple ways of solving this problem - each with its tradeoffs. Let's take a look.
Construct on first use idiom:
This idiom tries to make sure that there is always a fully constructed object whenever the static object in question is used. Following the examples in the previous section, this can be done by replacing all references to aObj
by a function call aObj()
which returns a reference to an object of type A
. In code it looks like this :
// Static initialization order problem
// File1.h
class A {
....
void doSomething() {
...
}
}
A& aObj();
//File1.cpp
A& aObj() {
static A *aObj = new A();
return *aObj;
}
// File2.cpp
class B {
B() {
/*
* Okay since calling aObj() gaurantees that
* static A *aObj = new A(); ran
*/
aObj().doSomething();
}
....
}
static B bObj;
bObj
can safely assume that calling aObj() returns a fully constructed aObj
since the line
static A *aObj = new A();
would have run on the function call and will give it a fully constructed object. Also, since the program never calls delete on aObj
, it is never destructed so it is also safe to use aObj
in bObj
's destructor. Though, this does mean that the memory allocated for aObj
always stays alive and valid throughout the lifetime of the program, which may or may not be a problem (it does get reclaimed by the OS after the program exits of course).
So, in which situation is this solution not apt? In the case that aObj
's destructor does something desirable. For example: when aObj
gets destructed - it writes to a log file / does something else that has some side effects.
Now you may ask - okay, why don't just I replace the static pointer in the aObj()
function call with a static aObj
object ?
A& aObj() {
static A aObj;
return aObj;
}
That still ensures that aObj
has been fully constructed by the time the function is called right? Right. However, it does not save us from the static de-initialization order problem. It is still possible that aObj
's destructor runs before bObj
's destructor.
There is an interesting trick that solves both of these problems. The Nifty Counter Idiom.
Using the Nifty Counter Idiom to solve Static init/de-init order problems
Reference : Nifty counter idiom presents the idea behind this idiom. Let's examine it.
The idea is to ensure that:
The static object being used gets constructed before any other static object in the translation unit that it is being used in.
The static object being used gets destructed after any other static object in the translation unit that it is being used in.
// File1.h
#pragma once
struct A {
A();
~A();
};
extern A& aObj;
static struct AInitializer {
AInitializer ();
~AInitializer ();
} aInitializer; // static initializer for every translation unit that aObj is used in
// File1.cpp
#include "File1.h"
#include <new> // Used for placement new
#include <type_traits> // Used for aligned_storage
static int niftyCounter; // this is zero initialized at load time
/*
* Memory for the static object aObj - memory itself is valid throughout the
* the lifetime of the program.
*/
static typename std::aligned_storage<sizeof (A), alignof (A)>::type
aObjBuf;
A& aObj = reinterpret_cast<A&> (aObj);
A::A ()
{
// Construct A
}
A::~A ()
{
/*
* Destruct A: with possible side effects
* like writing to a file.
*/
}
AInitializer::AInitializer ()
{
if (niftyCounter++ == 0) {
new (&aObj) A (); // use placement new operator
}
}
AInitializer::~AInitializer ()
{
if (--niftyCounter == 0) {
(&aObj)->~A(); // run the destructor
}
}
Let's try to understand what this code does: In the header file File1.h
has the definition of class A
first. After that, is have the definition of a class called AInitializer
. There is also a static object defined in the header file of type AInitializer
. This makes sure that the constructor for AInitializer
runs before the constructor for any other static object in the translation unit that File1.h
is included in (of course one has to include File1.h before any other static object's definition in source files) - remember: static objects in the same translation unit are constructed in order of declaration and destructed in the reverse order.
So now that AInitializer
is constructed before any other static objects in a translation unit - how can this be used this to our advantage? aObj
can be constructed in the constructor of AInitializer
! - which is what is happening in the lines
AInitializer::AInitializer ()
{
if (nifty_counter++ == 0) {
new (&aObj) A (); // use placement new
}
}
Note that the placement new operator is being used here instead of the new
operator to construct aObj
. Let's see what would happen if new
was used instead. The code would look like:
A& aObj;
A *aObjp = nullptr;
AInitializer::AInitializer ()
{
if (nifty_counter++ == 0) {
aObjp = new A ();
aObj = *aObjp; // Not okay! Cannot re-assign a reference
}
}
This doesn 't work since a reference needs to be defined and declared at the same time. That is precisely why the placement new
operator needs to be used on
static typename std::aligned_storage<sizeof (A), alignof (A)>::type
aObjBuf;
A& aObj = reinterpret_cast<A&> (aObj);
What this does is it allocates memory to fit an object of type A
and later assigns that to the reference. Now all what is left to be done is to actually construct the object in AInitializer
's constructor - which is what is done with the placement new operator.
Another question that may arise in readers' minds - Here there is a static object aObjBuf
- isn't that subject to the same de-initialization order problem that was mentioned in the second part of the Construct on first use idiom? The answer is that the memory for aObjBuf
stays alive and valid till the program is alive. Nothing happens in the construction of the memory. So it's valid to this.
This approach also makes that the static de-initialization order problem isn't hit - since the last AInitializer
object destructed will call the destructor of aObj
. That is guaranteed to run after any static objects in other translation units run since within the particular translation unit, the static object aInitializer
is declared before any other static object using aObj
- so it will get destructed in the reverse order - that is after the destructor for any other static objects have run.
There are some caveats here : this solution isn't the easiest to understand and implement . This is also not thread safe. More information can be found in the article on Nifty counters presented in the The C/C++ Users Journal, May, 1999 here.
Summary
Using statically initialized objects in C++ is tricky and should be done with care. There are multiple solutions and ways to get around the problem. The article went over some common solutions: the 'Construct on first use' idiom and the Nifty counter solution - and also their merits and de-merits.
Hope you enjoyed this article!
Subscribe to my newsletter
Read articles from Jayant Chowdhary directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Jayant Chowdhary
Jayant Chowdhary
I am a software engineer working on Android OS Camera Software.