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:

  1. Static Initialization: Composed of zero-initialization and constant initialization (evaluated at compile time).
  2. 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. A const variable 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. A constexpr variable 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.