constinit vs constexpr in C++: Timing and Mutability

In modern C++, both constexpr and constinit play critical roles in ensuring variables are initialized before runtime execution begins, but they serve distinct purposes regarding how data can be modified. While constexpr (introduced in C++11) forces both compile-time evaluation and absolute immutability, constinit (introduced in C++20) specifically guarantees compile-time static initialization without enforcing const semantics. This article breaks down the exact differences between constinit and constexpr, focusing directly on initialization timing, object mutability, and their practical applications.

Initialization Timing

The primary goal of initialization keywords in C++ is to control when an expression or variable is evaluated.

  • constexpr: A constexpr variable must be evaluated at compile time. The compiler computes the value during translation, substituting the result directly where applicable. It can be used in contexts requiring compile-time constants, such as template arguments, array bounds, or static_assert expressions.
  • constinit: A constinit specifier enforces that a variable with static or thread-local storage duration is initialized at compile time (static initialization) rather than during runtime (dynamic initialization). Its primary purpose is to eliminate the static initialization order fiasco, where the initialization order of global variables across different translation units is undefined. Unlike constexpr, constinit cannot be applied to local automatic variables.
Specifier Permitted Storage Duration Evaluation Phase
constexpr Automatic (local), static, or thread-local Compile time
constinit Static or thread-local only Compile time (guarantees static initialization)

Mutability

The most significant functional difference between the two specifiers is whether the resulting variable can be modified at runtime.

  • constexpr implies const: Declaring a variable constexpr implicitly marks it as const. Once initialized, its value is fixed in read-only memory or inlined, meaning it cannot be modified at runtime.
  • constinit does not imply const: A constinit variable is mutable by default unless explicitly combined with the const keyword (constinit const). This allows a program to initialize a global or thread-local variable safely before runtime starts, while still allowing the variable's state to be modified during program execution.
// Evaluated at compile time; immutable at runtime
constexpr int max_users = 100;
// max_users = 200; // Error: assignment of read-only variable

// Initialized at compile time; mutable at runtime
constinit int active_connections = 0;

void on_connect() {
    // Valid: constinit does not imply const
    ++active_connections; 
}

// Initialized at compile time; immutable at runtime
constinit const int buffer_size = 4096;

Usability in Constant Expressions

Because constinit allows mutability, it does not guarantee that the variable itself remains a constant expression throughout execution.

  • A constexpr variable can always be used in subsequent constant expressions, such as defining the size of a std::array or evaluating a static_assert.
  • A constinit variable (without an explicit const) cannot be used in a constant expression, because its value might change at runtime.
constexpr int a = 10;
constinit int b = 20;

// Allowed: 'a' is a compile-time constant expression
int arr1[a]; 

// Error: 'b' is not usable in a constant expression
// int arr2[b]; 

Summary of Differences

  • Choose constexpr when an object's value is known at compile time and should never change.
  • Choose constinit for global or thread-local variables to ensure they are initialized statically before runtime, avoiding initialization order issues while preserving the ability to update their values during execution.