Static Initialization Order
How C++ orders initialization and destruction of static-storage objects across translation units, the static initialization order fiasco, and the standard remedies.
Static initialization ordersince C++98The rules that determine when objects with static storage duration are initialized and destroyed, which are well defined within one translation unit but unspecified between translation units for ordinary non-local variables.
Overview
Objects with static storage duration (namespace-scope variables, static data members, and function-local statics) are initialized in two phases.
- Static initialization happens before any user code runs. It has two forms:
- Zero-initialization applies to every static object.
- Constant initialization applies when the initializer is a constant expression. Since C++11 this includes
constexprconstructors. Since C++20,constinitcan require it.
- Dynamic initialization runs arbitrary code, such as constructors and function calls, at startup. The ordering rules below apply to this phase.
The rules for dynamic initialization of non-local variables are:
- Within one translation unit (TU), variables are initialized in definition order. This does not apply to templates and implicitly instantiated static members, which are unordered.
- Across TUs, the order is unspecified. It typically follows link order, but this can change with the toolchain, the build system, or a parallel link.
- Since C++17,
inlinevariables are partially ordered. IfVis defined beforeWin every TU that defines both,Vis initialized first. - Deferral is allowed. An implementation may delay dynamic initialization until just before the first odr-use of something in the same TU. Do not depend on initialization happening before
main. - Destruction runs in reverse order of the completion of construction. Function-local statics are interleaved with
atexithandlers in the same reverse order.
The static initialization order fiasco (SIOF) occurs when the dynamic initializer of an object in one TU uses an object in another TU that may not yet be constructed. It is not undefined behavior in the formal sense, because the object is zero-initialized first. The code instead reads a zeroed, unconstructed object. A std::string in that state, or a container with a null internal pointer, will typically crash. The failure often appears only after an unrelated change to link order.
Syntax
// Constant initialization: no startup code, immune to SIOF
constexpr int kMax = 64; // C++11
constinit int g_counter = 0; // C++20: ill-formed if not constant-initialized
const char* const kName = "bigcpp"; // constant-initialized
// Dynamic initialization: ordering hazards apply
std::string g_prefix = make_prefix(); // runs at startup
inline std::vector<int> g_table = {}; // C++17: partially ordered
// Function-local static ("construct on first use")
Config& config() {
static Config instance; // thread-safe since C++11
return instance;
}Examples
The fiasco
// logger.cpp
#include <string>
std::string g_logPrefix = "[app] "; // dynamic init (std::string ctor)
// registry.cpp
#include <iostream>
extern std::string g_logPrefix;
struct Registry {
Registry() { std::cout << g_logPrefix << "registry up\n"; } // may see zeroed string
};
Registry g_registry; // may run BEFORE logger.cpp's initializerWhether this prints correctly depends on link order. Swapping the object files in the link command can change the behavior.
Fix 1: construct on first use
// logger.hpp
const std::string& logPrefix();
// logger.cpp
const std::string& logPrefix() {
static const std::string prefix = "[app] "; // initialized on first call
return prefix;
}
// registry.cpp
struct Registry {
Registry() { std::cout << logPrefix() << "registry up\n"; } // always valid
};
Registry g_registry;Initialization order is now driven by the call graph, not the linker. Since C++11, concurrent first calls are safe: one thread initializes while the others block. If the initializer throws, the next call retries. Recursive re-entry into the same initializer is undefined behavior.
Fix 2: make it constant-initialized (C++20)
struct Limits {
int maxConn;
constexpr Limits(int n) : maxConn(n) {} // constexpr ctor since C++11
};
constinit Limits g_limits{128}; // C++20: guaranteed no dynamic init, no SIOFconstinit does not make the object const. It can be modified at runtime, and it asserts only that the initial value is computed at compile time. For thread_local variables, it also lets the compiler skip the per-access initialization guard.
Fix 3: avoid the destruction-order problem with a leaky singleton
Construct-on-first-use fixes construction order but not destruction. If a global destructor calls logger() after logger's static has been destroyed, the result is a use-after-destruction.
Logger& logger() {
static Logger* p = new Logger; // intentionally never destroyed
return *p;
}Sanitizers may report this as a leak, so register it as intentional or use a suppression. A destructible alternative is to touch the dependency from the dependent's constructor. Because construction of logger's static completes first, it is destroyed after the dependent:
struct Registry {
Registry() { (void)logger(); /* ensures logger outlives *this */ }
~Registry() { logger().log("bye"); } // safe
};Fix 4: nifty counter (Schwarz counter)
This is the technique the standard streams use, so that std::cout is usable in the static constructors of any TU that includes <iostream>. It is useful for library-level globals that must be ready before any client static.
// stream.hpp
struct Stream { Stream(); ~Stream(); /* ... */ };
extern Stream& theStream; // reference to raw storage
struct StreamInit {
StreamInit();
~StreamInit();
};
static StreamInit streamInit; // one per including TU, ordered before client code
// stream.cpp
#include <new>
alignas(Stream) static unsigned char buf[sizeof(Stream)];
Stream& theStream = reinterpret_cast<Stream&>(buf);
static int nifty = 0;
StreamInit::StreamInit() { if (nifty++ == 0) new (&theStream) Stream; }
StreamInit::~StreamInit() { if (--nifty == 0) theStream.~Stream(); }Best Practices
- Prefer no dependency. Avoid non-trivial globals that depend on other globals. This is the most robust solution.
- Use
constexprorconstinitwherever possible (C++11 and C++20 respectively). Constant-initialized objects are fully formed before any dynamic initializer runs. - Wrap shared state in accessor functions that hold a function-local static. This is the default pattern for singletons, registries, and factory tables.
- Use
inlinevariables (C++17) for header-defined globals instead ofstaticcopies per TU orexternplus a separate definition. Partial ordering then gives you a predictable relationship within the same header. - Keep static constructors cheap and side-effect free. Do not do I/O, take locks, spawn threads, or allocate large resources. Do that work in an explicit
init()called frommain. - Design destruction deliberately. Decide whether a global must outlive other globals. If so, use a leaky singleton or force the construction order as shown above.
- Check with tools. Use
-fsanitize=addresswithASAN_OPTIONS=check_initialization_order=1:strict_init_order=1on Clang or GCC to catch SIOF at runtime. Static analyzers such as clang-tidy can flag some cases.
Common Pitfalls
- Relying on link order. It works until a build change, a switch to a static library, or a different linker reorders the objects. Static libraries are especially fragile, because TUs not referenced by the program may not be linked at all, and their initializers never run.
- Assuming zero-initialization means "safe". An integer reading 0 may look fine, but a zeroed
std::vectororstd::mutexis not a valid object. Even trivial-looking types are unsafe to use before their constructors have run. - Templates and static data members of templates. Their dynamic initialization is unordered even within a single TU. Since C++14, the same applies to variable templates.
- Recursive initialization of a function-local static. If the initializer calls back into the same accessor, the behavior is undefined, and in practice it often deadlocks.
- Thread-safety assumptions before C++11. Before C++11 the standard did not guarantee thread-safe local static initialization. Compilers offered it as an extension (
-fthreadsafe-staticsin GCC;/Zc:threadSafeInitin MSVC). Since C++11 it is mandatory, and these options can disable it (not recommended). - Using statics during
exit. Code running in destructors oratexithandlers may touch objects that have already been destroyed. A background thread still running at exit makes this worse. - Shared libraries. Initialization order between DSOs/DLLs is governed by the loader, not the language. Never perform cross-library dependencies in static constructors or in
DllMain. - Expecting
staticin a header to share state. A namespace-scopestatic(orconst) variable in a header gives each TU its own copy, each initialized separately.