Skip to content
C++

constexpr, consteval, and constinit

C++ gives you progressively stronger guarantees about when computation happens. constexpr says “this mayrun at compile time.” consteval says “this must run at compile time — a runtime call is a compile error.” constinitsays “this variable must have a compile-time initializer, but can be modified at runtime.” Together these three keywords let you express precise guarantees about when and how values are computed, and the compiler enforces those guarantees mechanically.

constexpr — optional compile-time evaluation

A constexpr entity is a promise that evaluation can occur at compile time when all inputs are compile-time constants. If the inputs are not known at compile time, the same code runs at runtime — there is no error. This dual-mode behaviour is both the power and the subtlety of constexpr: you write a function once and the compiler chooses when to evaluate it based on context.

constexpr double square(double x) { return x * x; }

// Called with a compile-time constant → evaluated at compile time:
constexpr double s1 { square(3.14) };  // compiler computes 9.8596…

// Called with a runtime value → evaluated at runtime:
double r;
std::cin >> r;
double s2 { square(r) };              // ordinary runtime call

This is worth contrasting with inline: a function marked constexpr is implicitly inline (the compiler needs to see the full definition at every call site). But it is not implicitly const — a constexpr function can take and return non-const types and can have side effects when called at runtime. A constexpr variable, on the other hand, is implicitly const — the variable is immutable after its initializer is evaluated.

Rules for constexpr variables (C++20)

For a variable to be declared constexpr, the compiler must be able to evaluate its initializer entirely at compile time. C++20 tightened these rules compared to earlier standards. Concretely, the type must be a literal type, the initializer must be a constant expression, and the variable must be destroyed by the end of its lifetime using constant destruction (no non-trivial destructor that does runtime work).

RequirementWhat it means in practice
Literal typeScalar types, references, arrays of literal types, or class types whose constexpr constructor initialises all members
InitialisedMust have an initializer — a constexpr variable without one is ill-formed
Constant expression initializerAll subexpressions in the initializer must themselves be constant expressions; no calls to non-constexpr functions
Constant destruction (C++20)If the type has a destructor, that destructor must also be constexpr and must itself be a constant expression when called

Rules for constexpr functions (C++20)

C++20 expanded what is allowed in a constexpr function substantially. Virtual functions, dynamic memory allocation, std::string, and even try/catch blocks are now permitted. The remaining restrictions are relatively narrow.

Allowed in C++20 constexpr functions

  • • Virtual functions and virtual base classes
  • • dynamic_cast and typeid
  • • new and delete (heap allocations that don't escape)
  • • std::string, std::vector (constexpr-capable containers)
  • • try/catch blocks (but throw makes call non-constant)
  • • Lambdas
  • • Changing the active member of a union

Still not allowed

  • • Coroutines (co_await, co_yield, co_return)
  • • goto or labels
  • • Variables of non-literal type in scope
  • • Static or thread-local variables
  • • Calls to non-constexpr functions (unless they are actually evaluated only at runtime)
  • • Undefined behaviour (UB makes the call non-constant)
// C++20: constexpr function with string and vector
constexpr std::vector<int> get_primes_under(int limit)
{
    std::vector<int> result;
    for (int n = 2; n < limit; ++n)
    {
        bool prime = true;
        for (int d = 2; d * d <= n; ++d)
            if (n % d == 0) { prime = false; break; }
        if (prime) result.push_back(n);
    }
    return result;   // heap allocation must not escape — OK here
}

// Compile-time: evaluated entirely at compile time
constexpr auto primes { get_primes_under(20) };
static_assert(primes.size() == 8);  // 2,3,5,7,11,13,17,19

constinit — guaranteed static initialisation

The static initialisation order fiasco is one of C++'s classic footguns. When two translation units each have a static-duration variable, and one's initializer depends on the other's, the order in which those initializers run across TUs is implementation-defined. If the dependent variable happens to be initialised first, you get undefined behaviour — and it often only manifests in certain link orders or on certain platforms.

constinit(C++20) prevents this by requiring the initializer to be a constant expression — meaning it must execute at compile time, before any dynamic initialisation phase. A static variable with a constant initializer has no dependence on any other TU's initializer: there is no ordering issue because the value is already baked in at link time.

// constinit: must have a compile-time initializer, but can be modified at runtime
constinit int connection_limit { 100 };   // constant init guaranteed

void reconfigure(int new_limit) {
    connection_limit = new_limit;   // OK — constinit doesn't imply const
}

// constexpr by contrast is immutable:
constexpr int MAX_CONNECTIONS { 1000 };   // can never be modified
// constinit prevents the SIOF:
// database.cpp
constinit int db_pool_size { 10 };   // constant init — safe regardless of link order

// connection.cpp
extern constinit int db_pool_size;
// using db_pool_size here is safe — it was initialised at compile time
// constinit rejects non-constant initializers:
int runtime_value() { return 42; }
constinit int bad { runtime_value() };  // ERROR: initializer is not constant
// Note: constexpr int bad { runtime_value() }; would also be an error
// but for slightly different reasons
KeywordImmutable?Initializer requirementApplies to
constexprYes (variable is const)Must be constant expressionVariables, functions, if statements
constinitNo (can be modified)Must be constant expressionStatic or thread-local variables only
constYesAny expressionVariables, member functions, pointers

consteval — immediate functions

A consteval function (also called an immediate function) must be evaluated at compile time. Every call to it must produce a constant — if the compiler cannot determine the result at compile time, the program is ill-formed and fails to compile. There is no runtime fallback as there is with constexpr. This makes consteval a stronger guarantee and a sharper tool: use it when runtime evaluation would be wrong, not just suboptimal.

consteval int compile_time_square(int x) { return x * x; }

// OK — argument is a compile-time constant:
constexpr int s1 { compile_time_square(3) };  // s1 = 9
int arr[compile_time_square(5)];              // arr has 25 elements

// ERROR — argument is a runtime variable:
int n;
std::cin >> n;
int s2 { compile_time_square(n) };   // compile error: n is not a constant expression

Several properties of immediate functions distinguish them from constexpr functions:

All declarations must include consteval

Unlike constexpr, where you can declare in a header and define elsewhere without repeating the keyword, every declaration of a consteval function must use consteval. The keyword is part of the function's identity.

Mutually exclusive with constexpr and constinit

A function cannot be both consteval and constexpr. They express different guarantees: constexpr allows runtime fallback; consteval forbids it.

Implicitly inline

Like constexpr functions, consteval functions are implicitly inline — the compiler must see the full definition at every call site. No runtime symbol is emitted because all calls are evaluated away at compile time.

Cannot take its address from non-consteval context

You cannot store a pointer to a consteval function and call it at runtime — because there is no runtime address to point to. Trying to form a pointer from a non-immediate context is a compile error.

Restrictions: no destructors, no allocation/deallocation functions

A consteval function cannot be a destructor (~T()), or an allocation function (operator new), or a deallocation function (operator delete). These are the only categories that are off-limits.

// Demonstrating consteval properties
consteval int square(int x) { return x * x; }

// Cannot take its address:
auto fp { &square };              // ERROR in non-immediate context
// But legal inside another consteval:
consteval auto get_fn() { return &square; }  // OK — immediate context

// All declarations must say consteval:
// In a header:
consteval int cube(int x);        // OK
// In a .cpp:
consteval int cube(int x) { return x * x * x; }  // must repeat consteval

// Would be a redeclaration error:
// int cube(int x) { return x * x * x; }  // missing consteval — error

Choosing between constexpr and consteval

The practical question is: should the function also be callable with runtime arguments? If the answer is yes (the function is a general utility that happens to be compile-time-capable), use constexpr. If the answer is no (a runtime call is semantically wrong, not just slower), use consteval.

Use constexpr when…

  • • The function is genuinely useful at runtime (math, utilities)
  • • You want a single implementation that serves both contexts
  • • The function can produce valid results for any input regardless of when it runs

Use consteval when…

  • • A runtime call is wrong by design (format string validation, index bounds baked into a type)
  • • You are computing array sizes, type indices, or template arguments
  • • You want the compiler to enforce that arguments must be compile-time constants
// constexpr: useful at both compile time and runtime
constexpr double lerp(double a, double b, double t) { return a + t * (b - a); }

// consteval: only makes sense at compile time — a runtime index is a bug
consteval int fibonacci_index(int n)
{
    if (n < 0) throw std::invalid_argument("negative index");
    int a = 0, b = 1;
    for (int i = 0; i < n; ++i) { int c = a + b; a = b; b = c; }
    return a;
}

// The throw above is legal in consteval — it makes the call non-constant,
// so any call with a negative argument is a compile error.
constexpr int fib10 { fibonacci_index(10) };   // OK: 55
// constexpr int fibN { fibonacci_index(-1) };  // compile error

Compile-time string processing with consteval

One of the most practically valuable uses of consteval is validating format strings or other compile-time string arguments. The classic example is a type-safe printf: you want to check that the format specifiers match the argument types before the program runs. With consteval, the validation happens at compile time and a mismatch is a compile error rather than undefined behaviour at runtime.

// Validate that a format string contains a specific specifier at compile time
consteval bool validate_format_string(std::string_view fmt, char expected_specifier)
{
    bool in_spec { false };
    for (char c : fmt)
    {
        if (c == '%') { in_spec = true; continue; }
        if (in_spec)
        {
            if (c == expected_specifier) return true;
            in_spec = false;
        }
    }
    return false;
}

// This is evaluated at compile time — fmt must be a string literal:
consteval void check_int_format(std::string_view fmt)
{
    if (!validate_format_string(fmt, 'd'))
        throw "format string must contain %d for int argument";
}

template <std::size_t N>
void safe_print_int(const char (&fmt)[N], int value)
{
    check_int_format(fmt);     // consteval — compile error if format is wrong
    std::printf(fmt, value);
}

// safe_print_int("%d items
", count);  // OK
// safe_print_int("%f items
", count);  // compile error: "format string must contain %d"
// Another pattern: consteval lookup table builder
consteval std::array<int, 256> build_ascii_digit_table()
{
    std::array<int, 256> table {};
    for (int i = 0; i < 256; ++i)
        table[i] = (i >= '0' && i <= '9') ? (i - '0') : -1;
    return table;
}

// Evaluated entirely at compile time — the binary contains the precomputed table:
constexpr auto digit_table { build_ascii_digit_table() };

int char_to_digit(char c) { return digit_table[static_cast<unsigned char>(c)]; }

std::string_view is particularly well-suited to compile-time string processing: its find(), substr(), size(), and iteration are all constexpr, making it a natural building block for consteval validators.

if consteval — branching on evaluation context

C++23 adds if consteval (also written if !consteval), which lets a constexpr function choose a different implementation path depending on whether it is currently being evaluated at compile time. This solves a concrete problem: sometimes the compile-time path needs to avoid intrinsics or platform-specific code that only works at runtime, while the runtime path can use those fast implementations freely.

#include <bit>

constexpr unsigned popcount_portable(unsigned x)
{
    if consteval
    {
        // Pure C++ path — safe for compile-time evaluation
        unsigned count { 0 };
        while (x) { count += x & 1; x >>= 1; }
        return count;
    }
    else
    {
        // Runtime path — uses compiler intrinsic for speed
        return std::popcount(x);    // maps to POPCNT instruction
    }
}

// Compile time — uses the portable path:
static_assert(popcount_portable(0b1011'1100) == 5);

// Runtime — uses std::popcount:
unsigned bits;
std::cin >> bits;
std::println("{} bits set", popcount_portable(bits));

Inside an if consteval branch, the compiler treats the code as if it were in an immediate (consteval) context — so you can call consteval functions there that would otherwise be illegal to call from a constexpr function.

Quick reference

KeywordEvaluates at compile time?Runtime fallback?Applies toImmutable?
constexpr (variable)YesN/A — always compile timeVariables with literal typeYes (implicitly const)
constexpr (function)Yes, if possibleYes — falls back to runtimeFunctions, constructors, lambdasN/A
constevalYes — requiredNo — runtime call = compile errorFunctions only (not destructors)N/A
constinitYes — initializer requiredN/A — applies to init, not valueStatic / thread-local variablesNo — value can change at runtime
if consteval (C++23)Selects compile-time branchSelects runtime branchInside constexpr functionsN/A
Sign in to track progress