Skip to content
C++
Language
Advanced

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++98

The 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.

  1. 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 constexpr constructors. Since C++20, constinit can require it.
  2. 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, inline variables are partially ordered. If V is defined before W in every TU that defines both, V is 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 atexit handlers 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

cpp
// 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

cpp
// 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 initializer

Whether 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

cpp
// 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)

cpp
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 SIOF

constinit 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.

cpp
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:

cpp
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.

cpp
// 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 constexpr or constinit wherever 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 inline variables (C++17) for header-defined globals instead of static copies per TU or extern plus 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 from main.
  • 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=address with ASAN_OPTIONS=check_initialization_order=1:strict_init_order=1 on 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::vector or std::mutex is 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-statics in GCC; /Zc:threadSafeInit in MSVC). Since C++11 it is mandatory, and these options can disable it (not recommended).
  • Using statics during exit. Code running in destructors or atexit handlers 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 static in a header to share state. A namespace-scope static (or const) variable in a header gives each TU its own copy, each initialized separately.

See Also