consteval and constinit
C++20 consteval forces compile-time function evaluation; constinit asserts static initialization without imposing const.
consteval / constinitsince C++20consteval declares an immediate function whose every call must produce a compile-time constant; constinit asserts that a variable with static or thread-local storage duration is initialized at compile time, while remaining mutable at runtime.
Overview
C++17 left two gaps in the constexpr story. First, a constexpr function silently degrades to runtime execution when its arguments are not constant expressions — there is no way to say "this must always be compile time." Second, there is no way to assert that a global variable is constant-initialized without also making it immutable. C++20 closes both gaps.
consteval makes a function an immediate function: every call site must occur within a constant-expression context. If the arguments are runtime values, the program is ill-formed. The compiler generates no machine code for the function body — it exists purely as a compile-time entity.
constinit is an initialization specifier, not a type qualifier. It instructs the compiler to reject the program if the initializer cannot be evaluated as a constant expression. It says nothing about mutability; after initialization, the variable behaves like an ordinary static.
Syntax
// consteval: applies to functions, constructors, and lambdas
consteval return-type function-name(params) { /* body */ }
// constinit: applies only to static or thread-local storage duration variables
constinit type variable = constant-expression;
constinit thread_local type tl_var = constant-expression; // C++20
constinit const type immutable = constant-expression; // const + constinit
// ill-formed:
// consteval int x = 42; // consteval is a function specifier only
// constinit int local = 42; // automatic storage duration — rejected
// constinit constexpr int y; // constexpr already implies constinitExamples
Enforcing compile-time hashing
The canonical motivation for consteval is preventing a function from silently running at runtime. An FNV-1a hash usable as switch case labels must be computed at compile time — consteval enforces this:
#include <cstdint>
#include <string_view>
consteval std::uint64_t fnv1a(std::string_view s) noexcept { // C++20
std::uint64_t h = 14695981039346656037ULL;
for (unsigned char c : s) {
h ^= c;
h *= 1099511628211ULL;
}
return h;
}
void dispatch(std::string_view cmd) {
switch (fnv1a(cmd)) { // ERROR: cmd is a runtime value
case fnv1a("open"): break; // OK: string literal is a constant expression
case fnv1a("close"): break;
}
}
// With constexpr, fnv1a(cmd) would silently fall back to runtime — hiding the bug.consteval constructors
consteval can be applied to constructors, requiring that every object of the type be constructed in a constant-expression context:
struct Endpoint {
const char* host;
int port;
consteval Endpoint(const char* h, int p) : host{h}, port{p} { // C++20
if (p <= 0 || p > 65535)
throw "port out of range"; // compile-time diagnostic
}
};
constexpr Endpoint api{"api.example.com", 443}; // OK
// Endpoint bad{"x", 99999}; // ERROR at compile time: port out of rangeconsteval lambdas
Lambdas accept consteval since C++20:
auto bit_mask = [](int bit) consteval -> unsigned { // C++20
if (bit < 0 || bit >= 32) throw "bit out of range";
return 1u << bit;
};
constexpr unsigned m = bit_mask(7); // 128, compile time
// unsigned n = bit_mask(runtime_b); // ERROR: not a constant expressionBranching on evaluation context
Inside a constexpr function you sometimes need separate compile-time and runtime paths. C++20 provides std::is_constant_evaluated(); C++23 replaces it with the cleaner if consteval:
#include <cmath>
// C++20: is_constant_evaluated() in a constexpr function
constexpr double fast_sqrt(double x) {
if (std::is_constant_evaluated()) { // C++20
// Newton-Raphson — suitable for constant evaluation
double r = x;
for (int i = 0; i < 32; ++i) r = 0.5 * (r + x / r);
return r;
}
return std::sqrt(x); // hardware instruction at runtime
}
// C++23: if consteval — syntactically unambiguous
constexpr double fast_sqrt_23(double x) {
if consteval { // C++23
double r = x;
for (int i = 0; i < 32; ++i) r = 0.5 * (r + x / r);
return r;
} else {
return std::sqrt(x);
}
}is_constant_evaluated() has a known trap: calling it inside a helper function reports true unconditionally because the helper call itself is a potential constant subexpression. if consteval (C++23) avoids this entirely.
constinit: preventing the static initialization order fiasco
Dynamic initialization order across translation units is unspecified. constinit eliminates the problem by forbidding dynamic initialization in the first place:
// --- network.cpp ---
// Dynamic init — order relative to other TUs is unspecified:
int max_connections = compute_max(); // could run after users of this value
// constinit — constant init — guaranteed before any dynamic init in the program:
constinit int max_connections = 1024; // C++20; initializer must be a constant expression
// --- worker.cpp ---
// Safe to read max_connections during its own dynamic initialization:
extern constinit int max_connections;
int worker_slots = max_connections / 4; // safe: max_connections already initializedconstinit with thread_local
constinit applies to thread_local variables, asserting compile-time initialization for each thread's copy:
constinit thread_local int request_count = 0; // C++20: per-thread, constant-initialized
void handle() {
++request_count; // mutable — fine
}constinit const vs constexpr
These are not interchangeable:
constexpr int A = 42; // const + constant-initialized; immutable
constinit int B = 42; // constant-initialized; mutable at runtime
constinit const int C = 42; // constant-initialized + const; immutable
B = 99; // OK
A = 99; // ERROR: constexpr variable is implicitly const
C = 99; // ERROR: const
// constinit const is most useful when constexpr would impose additional
// restrictions on the type (e.g., non-literal types with constexpr constructors).Comparison
| Specifier | Applies to | Compile-time required? | Mutable? | Notes |
|---|---|---|---|---|
const | variables, member functions | No | No | May have runtime initialization |
constexpr | variables, functions | Variables: yes; functions: maybe | No | Functions may also run at runtime |
consteval | functions only | Always | N/A | No machine code generated |
constinit | static/thread-local variables | Initialization only | Yes | No effect on type or runtime behavior |
Best Practices
Prefer consteval over constexpr whenever a function has no valid runtime use. The silent runtime fallback of constexpr is a correctness hazard; consteval makes violations a hard build error.
Use consteval for user-defined literals that encode compile-time semantics — format strings, SQL identifiers, enum-tag hashes, compile-time validated URLs. The cost of an invalid argument is a build failure, not a runtime panic.
Annotate all static globals with constinit when their initializers are constant expressions. It documents intent and catches accidental dynamic initialization if the initializer changes during refactoring.
Do not use constinit as a substitute for constexpr. If the variable should be immutable, say constexpr. constinit targets the pattern "initialized at compile time, mutated at runtime" — counters, flags, or configuration values that command-line parsing will overwrite.
For complex types that cannot satisfy constinit, use Meyers' singleton. When an initializer requires a non-trivial constructor that reads external state, the local-static pattern (static T& get() { static T inst; return inst; }) remains the correct SIOF fix.
Common Pitfalls
Applying consteval to variables. consteval int x = 42; is ill-formed. consteval is a function specifier only.
Applying constinit to local variables. constinit requires static or thread-local storage duration. Declaring constinit int n = 0; inside a function body is a compile error.
Storing a pointer to a consteval function. The standard prohibits forming a pointer to an immediate function in a non-constant context. You cannot store a consteval function in a std::function or function pointer and call it at runtime.
is_constant_evaluated() false positives in helper functions (C++20). A call to std::is_constant_evaluated() inside a nested helper reports true even when the outermost constexpr function is invoked at runtime, because the nested call is itself a potential constant subexpression. Prefer if consteval (C++23) or ensure the check is at the outermost function boundary.
Combining constinit and constexpr. constexpr implies constant initialization, so adding constinit is redundant and some compilers reject it as ill-formed. Choose one.
See Also
constexpr— the predecessor that permits but does not require compile-time evaluationstd::is_constant_evaluated()— C++20 mechanism for detecting evaluation context insideconstexprfunctionsif consteval— C++23 cleaner replacement foris_constant_evaluated()branching- Static initialization order fiasco — the broader problem
constinitaddresses for global variables - Constant expressions — the language rules governing what qualifies as a compile-time constant