Skip to content
C++
Language
since C++20
Intermediate

consteval and constinit

C++20 consteval forces compile-time function evaluation; constinit asserts static initialization without imposing const.

consteval / constinitsince C++20

consteval 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

cpp
// 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 constinit

Examples

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:

cpp
#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:

cpp
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 range

consteval lambdas

Lambdas accept consteval since C++20:

cpp
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 expression

Branching 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:

cpp
#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:

cpp
// --- 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 initialized

constinit with thread_local

constinit applies to thread_local variables, asserting compile-time initialization for each thread's copy:

cpp
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:

cpp
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

SpecifierApplies toCompile-time required?Mutable?Notes
constvariables, member functionsNoNoMay have runtime initialization
constexprvariables, functionsVariables: yes; functions: maybeNoFunctions may also run at runtime
constevalfunctions onlyAlwaysN/ANo machine code generated
constinitstatic/thread-local variablesInitialization onlyYesNo 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.

consteval vs constexpr Functions: The Forced-Compile-Time Guarantee

A constexpr function is an invitation to the compiler: evaluate me at compile time if the arguments permit it. A consteval function is a requirement: every call must be a constant expression or the program is ill-formed.

cpp
constexpr int ce_square(int x) { return x * x; }  // C++11
consteval int cv_square(int x) { return x * x; }  // C++20

int main() {
    int n = 5;

    // constexpr: silently degrades to runtime when argument is not constant
    int a = ce_square(n);   // OK β€” runs at runtime; no compile error
    int b = ce_square(3);   // OK β€” evaluates at compile time

    // consteval: runtime argument is a hard error
    // int c = cv_square(n);  // ERROR: n is not a constant expression
    constexpr int d = cv_square(3);  // OK β€” compile-time
    int e = cv_square(3);            // OK β€” 3 is a constant expression even here
}

The key distinction: passing a runtime value to a consteval function is always ill-formed, even when the result is discarded or not used as a constant expression. The compiler evaluates the arguments before accepting the call; if any argument is not a constant expression, the call is rejected.

No machine code is generated

The compiler never emits a callable symbol for an immediate function. This means:

  • Attempting to take the address of a consteval function in a non-constant context is ill-formed.
  • You cannot store a consteval function in a std::function, a plain function pointer, or pass it as a higher-order argument.
  • Template metaprogramming that needs a function pointer must use constexpr instead.
cpp
consteval int triple(int x) { return x * 3; }

using fn_t = int(*)(int);
// fn_t p = &triple;  // ERROR: forming pointer to immediate function
//                    // is only allowed in immediate contexts

// Workaround: wrap in a constexpr lambda at the call site
constexpr auto triple_val = triple(7);  // fine: 21

When to choose consteval over constexpr

SituationUse
Function must never run at runtime (hash for case labels, compile-time validated token)consteval
Function should run at compile time when possible, runtime otherwiseconstexpr
Function is purely a helper inside consteval contextconsteval (transitively required)
Function is used as a function pointer / stored in std::functionconstexpr only

consteval for Compile-Time Format String Validation

One of the most practical applications of consteval is validating string arguments at compile time, a pattern the standard library itself uses for std::format_string in C++20.

The idiom accepts a std::string_view or a custom wrapper, validates it inside a consteval function, and causes a compile-time error for invalid inputs β€” without any runtime overhead.

cpp
#include <string_view>
#include <stdexcept>

// A consteval validator rejects ill-formed format strings at compile time.
consteval std::string_view checked_fmt(std::string_view fmt) {
    // Count format placeholders β€” simplified illustration
    int braces = 0;
    for (std::size_t i = 0; i < fmt.size(); ++i) {
        if (fmt[i] == '{') {
            if (i + 1 < fmt.size() && fmt[i + 1] == '}') {
                ++braces;
                ++i;  // skip closing brace
            } else if (i + 1 < fmt.size() && fmt[i + 1] != '{') {
                throw std::invalid_argument("malformed placeholder");  // compile-time error
            }
        }
    }
    return fmt;
}

void log(std::string_view validated_fmt, auto&&... args);

void example() {
    log(checked_fmt("value = {}"), 42);       // OK: one placeholder
    // log(checked_fmt("value = {"), 42);     // ERROR at compile time: malformed
}

Before consteval, this required either a macro or std::is_constant_evaluated() gymnastics. With consteval, the validation is an ordinary C++ function that simply throws (or calls an ill-formed construct) for invalid input; the throw is a compile-time diagnostic, not a runtime exception.

Why std::is_constant_evaluated() is no longer needed here

In C++20, std::is_constant_evaluated() was the only way to branch inside a constexpr function based on whether the call was happening at compile or runtime. But it has subtle failure modes: calling it inside a nested helper always returns true because the helper's call itself forms a potential constant subexpression, even when the outermost invocation is at runtime.

consteval sidesteps the issue entirely. A consteval function does not need to query the evaluation context β€” it is only ever called at compile time, so you can use throw, raw pointer arithmetic, or any other constexpr-capable construct freely, knowing it will never run on actual hardware.

Nested consteval Calls and if consteval (C++23)

Calling consteval from consteval

A consteval function may call other consteval functions freely. It may also call constexpr functions; those calls are themselves forced into a constant-expression context, so they must evaluate at compile time.

cpp
consteval int clamp_power_of_two(int n, int lo, int hi) {
    // Both min and max could be constexpr; calling them from consteval
    // forces compile-time evaluation.
    if (n < lo) n = lo;
    if (n > hi) n = hi;
    // bit_floor equivalent β€” round down to nearest power of two
    int p = 1;
    while (p * 2 <= n) p *= 2;
    return p;
}

constexpr int buf_size = clamp_power_of_two(1000, 64, 4096);  // 512

if consteval (C++23, P1938)

C++23 introduced if consteval as a language-level mechanism to branch on whether the enclosing evaluation is manifestly constant-evaluated. It supersedes std::is_constant_evaluated() in virtually all practical uses.

cpp
#include <cmath>

constexpr double precise_log(double x) {
    if consteval {                       // C++23: true branch = compile time
        // Taylor series β€” usable in constant expressions
        // (simplified; real implementations use more terms)
        double result = 0.0;
        double term = (x - 1.0) / (x + 1.0);
        double t2 = term * term;
        double power = term;
        for (int k = 0; k < 20; ++k) {
            result += power / (2 * k + 1);
            power *= t2;
        }
        return 2.0 * result;
    } else {                             // false branch = runtime
        return std::log(x);              // hardware instruction via libm
    }
}

constexpr double ln2 = precise_log(2.0);   // compile-time Taylor series
double runtime_val = precise_log(3.7);     // runtime std::log

The if consteval block does not take parentheses (unlike if constexpr). This is intentional: consteval is a keyword, not a boolean expression.

if !consteval variant

The negated form if !consteval runs the first branch at runtime and the second at compile time β€” the mirror image:

cpp
constexpr int abs_val(int x) {
    if !consteval {          // C++23: branch taken at runtime
        return x < 0 ? -x : x;  // could use __builtin_abs for speed
    }
    // compile-time path below
    return x < 0 ? -x : x;
}

consteval functions callable from if consteval branches

A significant consequence: inside the if consteval branch, you may call consteval functions that would otherwise be forbidden from a constexpr context. This unlocks patterns where a compile-time path uses a stricter immediate function, and the runtime path uses a regular one.

cpp
consteval double fast_ct_exp(double x);   // immediate function β€” only usable compile-time

constexpr double safe_exp(double x) {
    if consteval {
        return fast_ct_exp(x);    // OK: inside if consteval, consteval calls are allowed
    } else {
        return std::exp(x);       // runtime
    }
}

constinit: Deep Dive

The initialization timeline

C++ initializes variables with static storage duration in two phases:

  1. Zero-initialization β€” every static variable is zeroed before any other initialization.
  2. Constant initialization β€” variables with constant-expression initializers are set to their final value during translation. This happens conceptually at compile time.
  3. Dynamic initialization β€” all remaining static variables are initialized at program startup, in an order that is only guaranteed within a single translation unit.

constinit forces a variable into phase 2. It is a static assertion on the initializer: the program is ill-formed if the initializer cannot be fully evaluated at compile time. No runtime constructor call, no std::atexit, no initialization ordering dependency.

cpp
// These are all constant-initialized β€” safe across TUs
constinit int g_timeout_ms   = 5000;           // C++20
constinit double g_pi        = 3.14159265358979;
constinit const char* g_name = "bigcpp";       // pointer, not the string, is constinit

// This would be ill-formed with constinit:
// constinit std::string g_title = "hello";    // ERROR: std::string constructor is not constexpr
// Use constexpr std::string_view instead:
constinit std::string_view g_title = "hello";  // OK: string_view has a constexpr constructor

constinit + thread_local: per-thread caches

Thread-local variables can be expensive if they require dynamic initialization, because the runtime must check an initialization guard on every access. constinit thread_local eliminates that guard: the compiler knows the initial value at translation time, so the thread-local can be placed in .tbss (thread-local BSS) or .tdata with no initialization code.

cpp
// Per-thread cache with guaranteed zero-initialization before any use
constinit thread_local std::size_t tl_alloc_count = 0;   // C++20
constinit thread_local void*       tl_last_ptr    = nullptr;

void* tracked_alloc(std::size_t n) {
    ++tl_alloc_count;          // mutable: fine
    tl_last_ptr = ::operator new(n);
    return tl_last_ptr;
}

Without constinit, the compiler cannot prove that tl_alloc_count's initialization is trivial and may emit a runtime check. With constinit, it is guaranteed to be zero before the first thread access, and no guard is needed.

constinit on extern declarations

An important and underused feature: constinit may appear on an extern declaration that provides no initializer. This acts as a documentation assertion β€” it tells the compiler "wherever this variable is defined, it must have been constinit-initialized." The compiler can enforce this for in-TU definitions and emit a warning/error if the definition lacks the guarantee.

cpp
// In a header (header.hpp):
extern constinit int g_max_threads;  // assertion: defined with constinit

// In exactly one .cpp:
constinit int g_max_threads = 32;    // definition satisfies the assertion

// consumer.cpp:
#include "header.hpp"
// Safe to use g_max_threads during dynamic init of other globals β€”
// its value is guaranteed to be set before dynamic init runs.
extern constinit int g_max_threads;
int g_worker_pool_size = g_max_threads / 2;  // safe

constinit does not prevent later assignment

This trips up developers expecting constinit to behave like const. The specifier only constrains the initializer. After initialization, the variable is fully mutable β€” assignments, increments, and swaps are all legal.

cpp
constinit int g_retry_limit = 3;    // constant-initialized

void configure(int retries) {
    g_retry_limit = retries;         // perfectly legal β€” constinit says nothing about runtime writes
}

If immutability is also required, combine with const:

cpp
constinit const int g_max_fds = 1024;    // constant-initialized AND immutable
// g_max_fds = 2048;                     // ERROR: const

This is distinct from constexpr because constexpr additionally requires the type to be a literal type, whereas constinit const only requires the initializer to be a constant expression. For types whose constructors are constexpr but which are not fully literal types (e.g., some custom allocators), constinit const may be applicable where constexpr is not.

See Also

  • constexpr β€” the predecessor that permits but does not require compile-time evaluation
  • std::is_constant_evaluated() β€” C++20 mechanism for detecting evaluation context inside constexpr functions
  • if consteval β€” C++23 cleaner replacement for is_constant_evaluated() branching
  • Static initialization order fiasco β€” the broader problem constinit addresses for global variables
  • Constant expressions β€” the language rules governing what qualifies as a compile-time constant