C++ Constinit: Prevent Dynamic Initialization Issues
Introduced in C++20, constinit is a keyword designed to
guarantee that variables with static or thread-local storage duration
are initialized at compile time. This article covers the mechanics of
constinit, the specific dynamic initialization problems it
solves—most notably the Static Initialization Order Fiasco—and how it
differs from const and constexpr.
The Problem: The Static Initialization Order Fiasco
In C++, non-local variables with static storage duration undergo
initialization before main() executes. This initialization
occurs in two primary phases:
- Static Initialization: Composed of zero-initialization and constant initialization (evaluated at compile time).
- Dynamic Initialization: Occurs at runtime when an expression cannot be evaluated at compile time.
When static variables across different translation units depend on
one another, a severe issue known as the Static Initialization
Order Fiasco can occur. The order of dynamic initialization
between distinct translation units is strictly undefined by the C++
standard. If variable A in file1.cpp relies on
variable B in file2.cpp during its dynamic
initialization, and file1.cpp initializes first,
A accesses B before B is
constructed, leading to undefined behavior and hard-to-diagnose runtime
crashes.
What is constinit?
The constinit specifier enforces that a variable with
static or thread-local storage duration must be initialized via constant
initialization (compile-time initialization).
If the compiler cannot evaluate the initializer expression at compile time, compilation fails immediately with an error:
constexpr int get_compile_time_value() {
return 42;
}
int get_runtime_value() {
return 100;
}
// Compiles: initialized at compile time
constinit int global_a = get_compile_time_value();
// Compilation Error: get_runtime_value() cannot be evaluated at compile time
// constinit int global_b = get_runtime_value();How
constinit Differs from const and
constexpr
Understanding where constinit fits alongside existing
specifiers is crucial:
const: Enforces runtime or compile-time immutability. Aconstvariable can still be dynamically initialized at runtime (e.g.,const int x = rand();), which means it does not solve initialization order problems.constexpr: Enforces both compile-time initialization and immutability. Aconstexprvariable cannot be modified after its creation.constinit: Enforces compile-time initialization without imposing immutability. The variable is initialized during the static phase, but it can be freely modified at runtime.
| Specifier | Evaluated at Compile Time? | Mutable at Runtime? |
|---|---|---|
const |
Optional | No |
constexpr |
Yes | No |
constinit |
Yes | Yes |
Preventing Dynamic Initialization Issues
constinit eliminates the Static Initialization Order
Fiasco by moving variable construction entirely out of the runtime
dynamic initialization phase and into the compile-time constant
initialization phase.
Because constant initialization always completes before any dynamic
initialization begins, any global or static variable marked
constinit is guaranteed to be fully constructed and safely
accessible by other translation units, even if those other units rely on
dynamic initialization.
// logger.hpp
struct Logger {
int log_level;
constexpr Logger(int level) : log_level(level) {}
};
// logger.cpp
// Guaranteed to be initialized at compile time, yet log_level can be changed later
constinit Logger global_logger(1);
// main.cpp
// Safe: global_logger is guaranteed to be ready before app_init runs
int app_init = global_logger.log_level; By turning subtle runtime initialization ordering bugs into immediate
compiler errors, constinit provides a robust, zero-overhead
mechanism for managing mutable global and thread-local state safely.