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

constexpr / consteval / constinit

Compile-time computation in C++. constexpr permits compile-time evaluation; consteval mandates it; constinit ensures safe static initialization of globals.

constexprsince C++11

constexpr applied to a variable guarantees compile-time constant initialization; applied to a function, it permits—but does not require—compile-time evaluation, depending on whether the call arguments are constant expressions.

Overview

Three keywords govern compile-time computation in modern C++:

KeywordIntroducedMeaning
constexprC++11Variable: compile-time constant. Function: may evaluate at compile time.
constevalC++20Function: must evaluate at compile time (immediate function).
constinitC++20Variable: constant initialization guaranteed; mutation still allowed.

The core distinction: constexpr is a permission, consteval is a mandate.


constexpr Variables

A constexpr variable must be initialized with a constant expression and is implicitly const:

cpp
constexpr int cache_line = 64;          // C++11
constexpr std::size_t N = 1024;

int buffer[N];                          // valid array size
std::array<int, N> arr;                 // valid template argument

For globals and header-level constants, prefer inline constexpr (C++17) to avoid ODR violations across translation units:

cpp
// constants.hpp
inline constexpr int max_connections = 256;   // C++17 — one definition, multiple TUs

Before C++17, the idiom was a static constexpr data member of a template class or a constexpr function returning the value.


constexpr Functions

C++11: The single-expression restriction

C++11 constexpr functions were severely limited: one return statement, no local variables, no loops. Anything non-trivial required recursion:

cpp
// C++11 — forced into recursion
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

C++14: Full function bodies

C++14 lifted essentially all restrictions. Loops, local variables, multiple return statements, and mutation of locals are all valid:

cpp
// C++14 — iterative, much more readable
constexpr long long factorial(int n) {
    long long result = 1;
    for (int i = 2; i <= n; ++i) result *= i;
    return result;
}

constexpr long long f10 = factorial(10);   // 3628800, computed at compile time
static_assert(f10 == 3628800);

int n = readInput();
long long rt = factorial(n);               // also valid — runs at runtime

To force compile-time evaluation, assign to a constexpr variable or use the result as a template argument. Without that, the compiler is free to defer to runtime.

Realistic pattern: compile-time lookup tables

A common production use is generating protocol or algorithm tables at compile time rather than at startup:

cpp
// C++14 — CRC-8 table computed entirely at build time
constexpr auto make_crc8_table() {
    std::array<uint8_t, 256> table{};
    for (int i = 0; i < 256; ++i) {
        uint8_t crc = static_cast<uint8_t>(i);
        for (int j = 0; j < 8; ++j)
            crc = (crc & 0x80) ? (crc << 1) ^ 0x07u : crc << 1;
        table[i] = crc;
    }
    return table;
}

constexpr auto crc8_table = make_crc8_table();   // zero runtime cost

constexpr classes

Any class with constexpr constructors and member functions can participate in constant expressions. C++20 extended this to cover destructors and virtual functions:

cpp
class Vec3 {
public:
    double x, y, z;
    constexpr Vec3(double x, double y, double z) : x(x), y(y), z(z) {}
    constexpr Vec3 operator+(Vec3 o) const { return {x+o.x, y+o.y, z+o.z}; }
    constexpr double dot(Vec3 o) const { return x*o.x + y*o.y + z*o.z; }
    constexpr double length_sq() const { return dot(*this); }
};

constexpr Vec3 a{1, 0, 0}, b{0, 1, 0};
static_assert(a.dot(b) == 0.0);
static_assert(a.length_sq() == 1.0);

C++20 also made std::string and std::vector constexpr, enabling compile-time string processing in constant expressions.


consteval (C++20)

consteval declares an immediate function — every call site must be a constant expression. There is no runtime fallback:

cpp
consteval int checked_port(int port) {   // C++20
    if (port < 1 || port > 65535)
        throw "port out of valid range";  // becomes a compile error
    return port;
}

constexpr int server_port = checked_port(8080);   // fine
// constexpr int bad = checked_port(99999);        // compile error: throw at compile time

This pattern turns configuration errors into diagnostics rather than runtime panics — a strictly better outcome for values fixed at build time.

Detecting the evaluation context

std::is_constant_evaluated() (C++20, <type_traits>) returns true when called inside a constant expression. This lets a single function use different implementations:

cpp
#include <cmath>
#include <type_traits>

constexpr double safe_sqrt(double x) {
    if (std::is_constant_evaluated()) {     // C++20
        // Newton–Raphson — no libm required
        double r = x / 2.0;
        for (int i = 0; i < 32; ++i)
            r = 0.5 * (r + x / r);
        return r;
    } else {
        return std::sqrt(x);               // hardware instruction at runtime
    }
}

static_assert(safe_sqrt(9.0) == 3.0);

C++23 added if consteval as a cleaner replacement — no function call, no subtle misuse:

cpp
constexpr double safe_sqrt_23(double x) {
    if consteval {                          // C++23
        double r = x / 2.0;
        for (int i = 0; i < 32; ++i) r = 0.5 * (r + x / r);
        return r;
    } else {
        return std::sqrt(x);
    }
}

Prefer if consteval over std::is_constant_evaluated() in C++23 — the latter can be inadvertently evaluated in a constant context in ways that produce surprising results.


constinit (C++20)

constinit guarantees that a variable with static or thread-local storage duration is initialized by a constant expression. Unlike constexpr, it does not imply const:

cpp
constinit int connection_count = 0;     // C++20 — safe to modify at runtime
constinit thread_local int depth = 0;   // thread-local, constant-initialized

// The three are distinct:
constexpr int A = 42;      // immutable + compile-time constant
const int B = compute();   // immutable, possibly runtime-initialized
constinit int C = 42;      // mutable, guaranteed compile-time initialized

The primary motivation is eliminating the static initialization order fiasco: when a non-constinit global's initializer invokes a function defined in another translation unit, initialization order is unspecified across TUs. constinit eliminates the problem by requiring the initializer to be a constant expression — no cross-TU dependencies possible.


if constexpr (C++17)

Inside a template, if constexpr selects branches at compile time. The non-taken branch is not instantiated, avoiding code that would be ill-formed for some template arguments:

cpp
template<typename T>
std::string type_desc(T val) {
    if constexpr (std::is_integral_v<T>) {           // C++17
        return "int:" + std::to_string(val);
    } else if constexpr (std::is_floating_point_v<T>) {
        return "float:" + std::to_string(val);
    } else {
        static_assert(std::is_convertible_v<T, std::string>,
                      "T must be string-convertible");
        return std::string(val);
    }
}

This replaces the majority of std::enable_if / SFINAE dispatch for function-body-level branching, with dramatically better readability and error messages.


Best Practices

Mark everything constexpr that can be. It costs nothing at runtime, enables callers in constant expressions, and forces the compiler to validate the function in constant context (catching latent UB).

constexpr implies inline for non-member functions. No need to write both.

Prefer inline constexpr for header constants (C++17). Avoids the subtle ODR issue that static constexpr members can produce in some linker scenarios.

Use consteval for values that must never be computed at runtime — protocol constants, validated configuration, compile-time-parsed formats. Turning runtime errors into build errors is always preferable.

Apply constinit to mutable globals that require safe initialization. It documents the intent and prevents SIOF without making the variable immutable.


Common Pitfalls

constexpr function ≠ compile-time guarantee

cpp
constexpr int square(int n) { return n * n; }

int x = readInput();
int r = square(x);   // runs at runtime — no compile-time evaluation

Assign to a constexpr variable or use as a template argument to force compile-time evaluation.

UB inside a constant expression is a compile error — exploit this

Undefined behavior (out-of-bounds access, signed overflow, null dereference) in a constant expression is ill-formed. This makes constexpr + static_assert a correctness tool:

cpp
constexpr std::array<int, 4> arr{10, 20, 30, 40};
// static_assert(arr[10] == 0);   // compile error: out of bounds — not UB silently ignored

At runtime the same access would be UB; at compile time it's a diagnostic. Move as much validation into constant expressions as possible.

Non-constexpr dependencies break the chain

If a constexpr function calls anything that is not itself constexpr — most C library functions, I/O, pre-C++20 new — the call site cannot appear in a constant expression:

cpp
constexpr int broken(int n) {
    return std::rand() % n;   // ERROR: std::rand is not constexpr
}

Compile-time cost at build time

Heavy constexpr computation shifts work from runtime to compile time, which is usually the goal — but deeply recursive templates or large-scale generation can significantly increase build times. If builds slow down after adding constexpr logic, use -ftime-report (GCC) or -ftime-trace (Clang) to identify the culprit.


See Also

  • Templatesif constexpr replaces most SFINAE-based dispatch
  • Concepts — constrain constexpr APIs with concept requirements
  • autoconstexpr auto for type-deduced compile-time constants