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.

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