C++ Static Initialization Order
Key takeaways
Globals in different .cpp files are dynamically initialized in an unspecified order, so one that reads another during startup can see an unconstructed object. Constant initialization is exempt; function-local statics, constinit and inline constexpr fix the rest, and destruction order has the mirror-image problem.
Introduction: “My program crashes when using global variables”
In C++, when using global variables from different files, the initialization order is undefined, which can lead to Static Initialization Order Fiasco where uninitialized variables are used.
// ❌ file1.cpp
// Example execution
std::vector<int> globalVec = {1, 2, 3};
// ❌ file2.cpp
extern std::vector<int> globalVec;
int globalSize = globalVec.size(); // ❌ globalVec might not be initialized!
int main() {
std::cout << globalSize << '\n'; // 0 or garbage value
}
The sections below explain why the order is undefined across translation units, which rules do apply, and how function-local statics, constexpr/constinit, explicit initialization, std::optional and Meyer’s Singleton avoid the problem, and how to choose between them.
What makes this bug memorable is that it rarely shows up where you are looking. The program works for months, then someone adds a file to the build, reorders sources in CMakeLists.txt, or switches from a static library to a shared one, and it crashes before main runs — the stack trace ends in __libc_csu_init, _GLOBAL__sub_I_..., or _initterm on Windows, with no user code on it except a global’s constructor. The first time I met it, the debugger showed a std::string with a null data pointer being copied, and nothing in the code looked wrong, because the code is not wrong in isolation; only the order is.
What is Static Initialization Order Fiasco?
Problem Occurrence
// config.cpp
#include <string>
std::string configPath = "/etc/config.txt";
// logger.cpp
#include <fstream>
extern std::string configPath;
std::ofstream logFile(configPath); // ❌ configPath might not be initialized!
// main.cpp
int main() {
logFile << "Hello\n"; // ❌ Crash or wrong file path
}
Problem:
- Initialization order of
configPathandlogFileis undefined - If
logFileis initialized first, it readsconfigPathbefore its constructor has run - Crash or incorrect behavior
“Opens a file with an empty string” is the optimistic description. Before its constructor runs, configPath is only zero-initialized memory. For libstdc++‘s std::string, all-zero bytes are not a valid empty string (the data pointer should point at the internal buffer), so reading it is undefined behavior: sometimes an empty path, sometimes a crash, and it can change with the compiler version or standard library. Worse, after logFile is constructed from the garbage, configPath’s constructor runs later and overwrites it, so inspecting configPath in a debugger afterwards shows the correct value and hides the cause.
Initialization Order Rules
Rule 1: Within the Same Translation Unit
// file.cpp
int a = 10;
int b = a + 5; // ✅ a is initialized first (declaration order)
int main() {
std::cout << b << '\n'; // 15
}
Rule: Within the same file, initialization follows declaration order.
Rule 2: Across Different Translation Units
// file1.cpp
int computeX(); // defined elsewhere, runs at startup
int x = computeX(); // dynamic initialization
// file2.cpp
extern int x;
int y = x + 5; // ❌ x might still be 0 here!
Rule: Across different files, the order of dynamic initialization is unspecified.
The word dynamic matters. C++ initializes globals in two phases. Static initialization happens first, conceptually before any code runs: every global is zero-initialized, and globals whose initializer is a constant expression (int x = 10;, const char* p = "abc";, a constexpr constructor call) get their final value right away — in practice they are baked into the executable’s data section. Only then does dynamic initialization run constructors and non-constant initializers, in declaration order within each file and in an unspecified order across files.
So if file1 contained plain int x = 10;, y would reliably be 15: x is constant-initialized and never participates in the race. The fiasco requires at least one global whose value is computed at runtime — a std::string, a std::map, anything with a non-constexpr constructor, or an initializer that calls a function. That is also why the problem often appears only after an innocent-looking refactor turns a literal into a function call.
In practice, the order across files usually follows the order in which the linker sees object files, which is why reordering sources or libraries changes behavior. You cannot rely on that; it differs between toolchains and between static and dynamic linking.
Solution: Function-Local Static Variables
Solution 1: Wrap in Functions
// config.cpp
#include <string>
std::string& getConfigPath() {
static std::string configPath = "/etc/config.txt";
return configPath;
}
// logger.cpp
#include <fstream>
std::ofstream& getLogFile() {
static std::ofstream logFile(getConfigPath()); // ✅ Initialized on first call
return logFile;
}
// main.cpp
int main() {
getLogFile() << "Hello\n"; // ✅ Safe
}
Advantages:
- Lazy Initialization (initialized on first call)
- Thread-safe since C++11 (Magic Statics)
- Solves initialization order problem
This is often called the “construct on first use” idiom. The function-local static is initialized the first time control passes through its declaration, so whoever asks first triggers construction, and dependencies are constructed in the order they are actually needed. The C++11 guarantee means that if two threads call getConfigPath() simultaneously, one constructs the object and the other waits; compilers implement it with a guard variable checked on each call, which costs a load and a branch after the first call — negligible except in the tightest loops. (Old MSVC versions before VS2015 did not implement the guarantee, and /Zc:threadSafeInit- turns it off.)
Two caveats. If the constructor throws, the static is not considered initialized and the next call tries again. And a cycle — A’s initializer calls getB(), whose initializer calls getA() — is undefined behavior; GCC’s implementation detects the recursive entry and throws __gnu_cxx::recursive_init_error, while other implementations may deadlock. Function-local statics fix ordering, but not circular dependencies.
Solution 2: constexpr (C++17 inline variables)
// config.h — included by every file that needs the path
inline constexpr const char* configPath = "/etc/config.txt";
// logger.cpp
#include "config.h"
#include <fstream>
std::ofstream logFile(configPath); // ✅ configPath is constant-initialized
Advantages:
- Compile-time initialization
- No initialization order problem
Disadvantages:
- Only works with literal types
A constexpr variable must have its initializer at the point of declaration, so the tempting extern constexpr const char* configPath; in another file does not compile. Before C++17 the usual workaround was a plain constexpr in the header, which gives each translation unit its own internal copy (harmless for a pointer or number). inline constexpr makes it a single entity across the program.
When the value cannot be constexpr but the variable should still never take part in dynamic initialization, C++20’s constinit is the tool: constinit int maxConnections = 64; guarantees constant initialization and is a compile error if the initializer would require runtime code. It turns the fiasco from a runtime surprise into a build failure, which is exactly where you want it. It does not make the variable const; it only fixes how it starts life.
std::string is not usable here before C++20 (and even in C++20 a constexpr std::string cannot outlive constant evaluation), which is why the example uses const char* or, better, std::string_view.
Solution 3: An Explicit Initialization Function
When you want the order written down in one place, make the globals pointers and create the objects from main:
// globals.cpp
std::unique_ptr<std::string> configPath; // constant-initialized: starts as nullptr
std::unique_ptr<std::ofstream> logFile;
void initGlobals() {
configPath = std::make_unique<std::string>("/etc/config.txt");
logFile = std::make_unique<std::ofstream>(*configPath); // order is visible here
}
// main.cpp
int main() {
initGlobals();
*logFile << "Hello\n";
}
This works because a null pointer is constant-initialized, so the globals are valid from the very start; the objects themselves are created inside main, where statement order is ordinary program order. std::unique_ptr’s default constructor is constexpr, so it keeps that property while also cleaning up at exit, which raw new/delete pairs do not do when main leaves through an exception. The weakness is the one to check before choosing it: any other global’s constructor that runs before main and touches logFile dereferences a null pointer. This style fits programs, often older ones, where no dynamic initializer depends on these objects and you want the startup order to be readable in one function.
Solution 4: std::optional for Deferred Construction (C++17)
#include <optional>
std::optional<std::string> configPath; // empty, constant-initialized
std::optional<std::ofstream> logFile;
void initConfig() {
configPath = "/etc/config.txt";
logFile.emplace(*configPath);
}
int main() {
initConfig();
if (logFile) {
*logFile << "Hello\n";
}
}
The reason this is safe is the same as for Solution 3: std::optional’s default constructor is constexpr, so an empty global optional is constant-initialized even when the contained type has a non-trivial constructor. Compared with pointers, the object lives inline with no heap allocation and is destroyed automatically at exit, and if (logFile) tells you whether initialization happened. That check is the price: *logFile on an empty optional is undefined behavior, and .value() throws std::bad_optional_access.
Singleton Pattern
Meyer’s Singleton (Recommended)
class Logger {
private:
Logger() {
file_.open("/var/log/app.log");
}
std::ofstream file_;
public:
// Delete copy and move
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
static Logger& getInstance() {
static Logger instance; // ✅ Initialized on first call (thread-safe)
return instance;
}
void log(const std::string& msg) {
file_ << msg << '\n';
}
};
int main() {
Logger::getInstance().log("Hello"); // ✅ Safe
}
Advantages:
- Thread-safe (C++11 and later)
- No initialization order problem
- Lazy Initialization
It is the function-local static applied to a class: the private constructor and deleted copy operations guarantee that the instance inside getInstance() is the only one. Two practical costs come with it. Code that calls Logger::getInstance() directly cannot be handed a different logger in a unit test, so a common compromise is to call the accessor only near the top of the program and pass a Logger& down. And with shared libraries, if getInstance() is defined inline in a header and several DLLs or .so files built with hidden symbol visibility include it, each library can end up with its own copy of the static unless the function is defined in one library and exported. When a singleton behaves as if it had been reset, that is worth checking first.
The Destruction-Order Problem
Meyer’s Singleton solves construction order, but statics are destroyed in the reverse order of construction when the program exits, and that has its own fiasco. Suppose another global’s destructor logs a final message:
struct ConnectionPool {
~ConnectionPool() { Logger::getInstance().log("pool closed"); }
};
ConnectionPool gPool; // constructed before Logger was first used
If Logger’s instance was created after gPool, it is destroyed before gPool, and ~ConnectionPool calls log on a destroyed object — a crash during shutdown, often reported as an access violation after main has returned. Such crashes are easy to dismiss because the program “already finished”, but they can lose the last buffered log lines or corrupt files being flushed.
The Intentionally Leaked Singleton
Logger& getLogger() {
static Logger* instance = new Logger(); // never deleted, on purpose
return *instance;
}
This version is often presented as a mistake, but it is a deliberate idiom for exactly the problem above: the object is never destroyed, so it is usable from any destructor during shutdown. The costs are real — the destructor never runs, so the ofstream is not closed by the class (the OS reclaims the memory and closes the file descriptor at process exit, but buffered data that was never flushed is lost), and leak checkers report it unless suppressed. Which to use depends on the object: for a logger or a registry that other destructors might touch, leaking is often the safer choice (Google’s C++ style guide recommends it for non-trivially-destructible globals); for objects that must flush or release external resources cleanly, use Meyer’s Singleton and make sure nothing touches it after main returns.
Initialization order rules and how to find violations
| Scope | Initialization Order |
|---|---|
| Within same file | Declaration order |
| Across different files | Undefined |
| Function-local static | On first call |
| constexpr / constinit / constant initializer | Before any dynamic initialization |
| Destruction | Reverse of construction |
Choosing a Fix
| Fix | Works when | Main cost |
|---|---|---|
| Function-local static | Almost always; the default choice | Guard check per call, destruction order still reverse |
constexpr / inline constexpr / constinit | The value is known at compile time | Literal types only (string_view, not std::string) |
| Meyer’s Singleton | You need exactly one object with behavior | Global state, harder to test |
| Explicit init function | No global constructor uses the object before main | Null dereference if someone does; order must be maintained by hand |
std::optional global | Construction must wait for runtime input | Every use needs an if or risks UB |
The order of preference is roughly: avoid the global; if you cannot, make it constant-initialized; if it needs runtime construction, wrap it in a function.
Finding It in an Existing Codebase
AddressSanitizer can detect this directly: build with -fsanitize=address and run with ASAN_OPTIONS=check_initialization_order=1 (plus strict_init_order=1 for the stricter mode). It then reports initialization-order-fiasco when a dynamic initializer in one file reads a global from another file that has not been initialized yet — even in builds where the order happens to be right, which is what makes it useful. A cheap static check is to search for globals with non-trivial types (grep for ^std:: or ^static std:: at file scope) and ask for each one whether any other file’s initializer uses it.
Because the order across files depends on link order, a program can work for years and break when a build system change reorders object files. That is why the fix belongs in the code (function-local statics, constinit, inline constexpr) rather than in the link order.
Frequently Asked Questions (FAQ)
Q. Does the fiasco also affect globals defined in the same .cpp file?
A. No. Within one translation unit, dynamically initialized globals are initialized in the order they are defined, so the problem appears only when a global in one .cpp file reads a global defined in another .cpp file during its own initialization. The order across translation units is unspecified and can change with link order, which is why a program can work in one build and crash in another. Wrapping the dependency in a function-local static moves its initialization to first use.
Related Articles
- C++ Linkage and Storage Duration
- C++ Dynamic Initialization
- C++ static Members
- C++ Undefined Behavior
- C++ Stack Overflow
- C++ constexpr Functions
- C++ Series