C++ Initialization Order: Static Phases, Cross-TU Globals, Bases and Members
Key takeaways
Static-storage objects are zero-initialized, then constant-initialized, then dynamically initialized at startup. Within one .cpp file dynamic initialization runs top to bottom; across files the order is unspecified, which is the static initialization order fiasco. Inside a class, bases and members are initialized in declaration order, never in initializer-list order.
Why initialization order matters
Most of the time, you never think about when a variable is initialized. Locals are initialized when execution reaches them, and that is obvious from the code. The two places where the order is not obvious, and where bugs hide, are global and static objects, which are initialized before main in an order the code does not spell out, and class members and bases, which are initialized in an order that does not match what the constructor’s initializer list appears to say.
Here is the classic example of the first problem. Two files, each perfectly reasonable on its own:
// file1.cpp
int compute() { return 100; }
int x = compute(); // dynamic initialization: runs at startup
// file2.cpp
#include <cstdio>
extern int x;
int y = x * 2; // reads x, which may not be initialized yet
int main() { std::printf("x=%d y=%d\n", x, y); }
Built with MinGW g++ 10.3, the result depends only on the order the files appear on the command line:
$ g++ file1.cpp file2.cpp && ./a.out
x=100 y=0
$ g++ file2.cpp file1.cpp && ./a.out
x=100 y=200
The y=0 run is not a random value: x had been zero-initialized but not yet dynamically initialized when y’s initializer read it. That is the static initialization order fiasco, and the uncomfortable part is that the program has no compiler error, no warning, and a result that changes when someone reorders source files in a build script. Which order a given toolchain picks is an implementation detail; do not rely on either one.
The three phases for static storage
Every variable with static storage duration (namespace-scope variables, static class members, function-local statics) goes through these steps:
static int a; // zero-initialized: a == 0
constexpr int b = 10; // constant initialization
constinit int c = 20; // C++20: constant initialization, checked
int func() { return 30; }
int d = func(); // zero-initialized first, then dynamic initialization
| Phase | When | What it covers |
|---|---|---|
| Zero initialization | Before anything else | Every static-storage object without constant initialization |
| Constant initialization | Before any dynamic initialization, usually at compile time | Initializers that are constant expressions |
| Dynamic initialization | At startup, before main (or lazily, see below) | Everything else |
Two details matter here. First, constant initialization is not limited to constexpr variables: int limit = 100; at namespace scope is constant-initialized too, because 100 is a constant expression. That means that if file1.cpp above had said int x = 100; instead of int x = compute();, there would be no fiasco at all; x would already hold 100 in the binary’s data section. The fiasco needs a dynamic initializer on the variable being read.
Second, “zero first” is what makes the bug quiet. A dynamically initialized int read too early reads 0, and a std::string or std::map read too early is an all-zero object whose constructor has not run, which is often enough to look empty and then crash on the first insertion.
Order within one translation unit
Within one .cpp file, dynamic initialization of ordinary namespace-scope variables happens in the order their definitions appear:
int a = compute(); // first
int b = a * 2; // second: sees a already initialized
int c = b + a; // third
That guarantee is why “put the globals that depend on each other in the same file” is a legitimate fix. There are exceptions: static data members of class templates and variables defined from templates have unordered initialization, and C++17 inline variables are only partially ordered. Do not rely on ordering for those.
Order across translation units
For variables in different translation units, the standard leaves the order unspecified. In practice it depends on the linker and the order object files are passed to it, which is exactly what the command-line experiment above shows. Real-world versions look like this: a global Logger defined in logger.cpp, and a global Database defined in db.cpp whose constructor calls logger.log("connecting"). It works for months, then a build change swaps the link order and the program crashes before main.
A related trap is destruction order. Static objects are destroyed after main returns, in reverse order of the completion of their construction. If Database’s destructor logs through the global Logger, and Logger was constructed after Database, then Logger is destroyed first and the destructor calls a member function on a dead object.
On Linux, AddressSanitizer can catch the cross-file read at run time: build with -fsanitize=address and run with ASAN_OPTIONS=check_initialization_order=1, and it reports initialization-order-fiasco when a dynamic initializer reads a global from another file that has not been initialized yet. I have found it worth enabling in CI for any program with non-trivial globals, because the bug otherwise only appears after an unrelated build change.
Member and base initialization order
The second kind of order problem is inside a class. The rule is fixed, and the initializer list does not change it:
- Virtual base classes, depth-first, left to right.
- Direct non-virtual bases, in the order they appear in the class head.
- Non-static data members, in the order they are declared in the class body.
- The constructor body.
Destruction runs the same list in reverse. A quick demonstration:
#include <iostream>
struct P { P(const char* s) { std::cout << s << '\n'; } };
struct V { V() { std::cout << "virtual base V\n"; } };
struct B1 { B1() { std::cout << "base B1\n"; } };
struct B2 : virtual V { B2() { std::cout << "base B2\n"; } };
struct D : B1, B2 {
P second{"member second"};
P first{"member first"};
D() : first("member first"), second("member second") { std::cout << "D body\n"; }
};
int main() { D d; }
virtual base V
base B1
base B2
member second
member first
D body
The initializer list names first before second, but second is declared first, so it is built first. This is where a real bug appears:
struct Data {
int b;
int a;
Data() : a(10), b(a * 2) {} // b is initialized first, from an uninitialized a
};
The constructor reads as “set a to 10, then b to 20”, but b is declared first, so b(a * 2) runs while a is still indeterminate. GCC with -Wall catches it with two warnings:
warning: 'Data::a' will be initialized after [-Wreorder]
warning: 'int Data::b' [-Wreorder]
warning: when initialized here [-Wreorder]
warning: '*<unknown>.Data::a' is used uninitialized in this function [-Wuninitialized]
The fix is to declare a before b, or to avoid having one member’s initializer depend on another at all (compute the value once in a local and pass it to both). I have seen this one introduced by an innocent refactor: someone reorders members to reduce padding, the initializer list still “looks right”, and a member that used to be initialized from its neighbour is now initialized from garbage. -Wreorder is part of -Wall in GCC and on by default in Clang, and it is worth keeping it as an error.
Fixing the cross-file problem
Make it constant initialization
If the value can be computed at compile time, the order problem disappears, because constant initialization finishes before any dynamic initialization starts. C++20’s constinit lets you require that:
// config.h
inline constexpr int base = 100;
// file2.cpp
#include "config.h"
constinit int y = base * 2; // guaranteed to be done before any dynamic init
constinit does not make the variable const; it only forbids a dynamic initializer, and the compiler enforces it. Try to use it on the original example and you get an error rather than a silent bug:
error: 'constinit' variable 'y' does not have a constant initializer
error: the value of 'x' is not usable in a constant expression
note: 'int x' is not const
The same happens if the initializer calls a non-constexpr function: error: call to non-'constexpr' function 'int compute()'. That makes constinit a cheap way to document and check “this global is safe to read from other files’ initializers”.
Use a function-local static
When the object really needs runtime construction, wrap it in a function:
class Logger {
public:
static Logger& instance() {
static Logger logger; // constructed the first time instance() runs
return logger;
}
void log(const char* msg);
private:
Logger() = default;
};
class Database {
public:
Database() { Logger::instance().log("Database created"); }
};
Whichever global asks first causes construction, so the dependency order is correct by definition. Since C++11 the initialization is thread-safe: if two threads call instance() at once, one constructs and the other waits. The cost is a check on each call, which is almost never measurable.
It does not solve destruction order. The function-local Logger is destroyed at exit like any other static, so a global whose destructor calls Logger::instance() after the logger has been destroyed is still undefined behavior. When that matters, a deliberately leaked instance (static Logger* logger = new Logger;) is never destroyed, which trades a tidy shutdown for safety.
Make initialization explicit
The most boring fix is often the best one: do not have globals with non-trivial constructors at all. Construct the logger, database and cache in main in the order you want, and pass them to the code that needs them. The order is then visible in one place, testable, and immune to link order.
Initialization order by scope
| Scope | Initialization order |
|---|---|
| Static storage, constant initializer | Before any dynamic initialization |
| Dynamic init, same translation unit | Order of definition (with template and inline exceptions) |
| Dynamic init, different translation units | Unspecified |
| Function-local static | On first execution of its declaration |
| Class: virtual bases, bases, members | Declaration order, never initializer-list order |
| Destruction | Reverse of completed construction |