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.


constexpr containers and algorithms (C++20)

C++20 made std::vector and std::string fully constexpr. This means heap-allocated data structures can now be created, populated, and destroyed entirely within a constant expression — as long as no allocation escapes to runtime.

cpp
// C++20 — vector and string in constant expressions
#include <vector>
#include <algorithm>
#include <numeric>

constexpr int sum_of_squares(int n) {
    std::vector<int> v(n);
    std::iota(v.begin(), v.end(), 1);          // fill with 1, 2, ..., n
    int total = 0;
    for (int x : v) total += x * x;
    return total;
}

static_assert(sum_of_squares(4) == 1 + 4 + 9 + 16);   // 30

Most <algorithm> functions are also constexpr in C++20, so sorting, searching, and transforming compile-time collections works out of the box:

cpp
// C++20 — compile-time sort + binary search
#include <array>
#include <algorithm>

constexpr auto sorted_primes() {
    std::array<int, 6> p{11, 2, 7, 3, 5, 13};
    std::sort(p.begin(), p.end());
    return p;
}

constexpr auto primes = sorted_primes();   // {2, 3, 5, 7, 11, 13} — compile time
static_assert(std::binary_search(primes.begin(), primes.end(), 7));

The key constraint: all memory allocated inside a constexpr evaluation must be deallocated before that evaluation completes. "Transient allocation" is fine; persistent heap state escaping to runtime is not.


constexpr virtual functions (C++20)

C++20 permits virtual functions to also be constexpr. Before this change, any class with a virtual dispatch mechanism was barred from constant expressions, severely limiting compile-time polymorphism.

cpp
// C++20 — virtual functions in constant expressions
struct Shape {
    constexpr virtual double area() const = 0;
    constexpr virtual ~Shape() = default;
};

struct Circle : Shape {
    double r;
    constexpr explicit Circle(double r) : r(r) {}
    constexpr double area() const override { return 3.14159265358979 * r * r; }
};

struct Square : Shape {
    double side;
    constexpr explicit Square(double s) : side(s) {}
    constexpr double area() const override { return side * side; }
};

constexpr double total_area() {
    Circle c{2.0};
    Square s{3.0};
    const Shape* shapes[] = {&c, &s};
    double sum = 0;
    for (const Shape* sh : shapes) sum += sh->area();
    return sum;
}

static_assert(total_area() > 0.0);

The caveat is that dynamic_cast and typeid remain off-limits in constant expressions, so runtime-polymorphism idioms that rely on RTTI still cannot be evaluated at compile time.


constexpr destructors (C++20)

Destructors can be marked constexpr in C++20, completing the lifecycle support for types used in constant expressions. Without this, even types with trivially-correct cleanup could not be used inside constexpr contexts if they had user-defined destructors.

cpp
// C++20 — constexpr destructor
class ScopedBuffer {
    int* data;
    std::size_t size;
public:
    constexpr explicit ScopedBuffer(std::size_t n)
        : data(new int[n]{}), size(n) {}

    constexpr ~ScopedBuffer() {
        delete[] data;          // valid inside a constant expression
    }

    constexpr int& operator[](std::size_t i) { return data[i]; }
    constexpr std::size_t length() const { return size; }
};

constexpr int sum_buffer(std::size_t n) {
    ScopedBuffer buf(n);
    for (std::size_t i = 0; i < n; ++i) buf[i] = static_cast<int>(i + 1);
    int s = 0;
    for (std::size_t i = 0; i < n; ++i) s += buf[i];
    return s;   // buf destroyed here via constexpr destructor
}

static_assert(sum_buffer(5) == 15);

The rule is identical to constexpr allocation: the destructor must run and fully release all resources before the constant evaluation completes.


std::is_constant_evaluated

std::is_constant_evaluated() (C++20, <type_traits>) lets a single function branch between a compile-time implementation and a runtime-optimized one. This is the standard solution when the best compile-time algorithm differs from the best runtime algorithm — for example, math functions that use Newton–Raphson at compile time but call intrinsics at runtime.

cpp
// C++20 — dual-path implementation
#include <cmath>
#include <type_traits>

constexpr double my_abs(double x) {
    if (std::is_constant_evaluated()) {
        return x < 0.0 ? -x : x;   // no libm dependency
    } else {
        return std::fabs(x);        // hardware-mapped intrinsic
    }
}

static_assert(my_abs(-3.14) == 3.14);   // compile-time path

A critical subtlety: the argument to std::is_constant_evaluated() must not itself be a constant expression — always call it bare, never inside static_assert or as a template argument, or it will always return true and the runtime branch is never compiled:

cpp
// WRONG — always true, runtime branch dead code
static_assert(std::is_constant_evaluated());   // trivially true here

// CORRECT — context-sensitive
constexpr bool probe() { return std::is_constant_evaluated(); }

In C++23, if consteval supersedes most uses of std::is_constant_evaluated() and avoids this pitfall entirely.


consteval and constinit

consteval and constinit address two distinct problems that constexpr alone cannot solve. consteval enforces compile-time-only evaluation; constinit enforces safe initialization of mutable globals.

consteval — immediate functions

Every call to a consteval function must occur in a constant expression. This is stronger than constexpr, which merely permits compile-time evaluation:

cpp
// C++20 — consteval mandates compile-time evaluation
consteval int bits_for(unsigned max_val) {
    int bits = 0;
    while ((1u << bits) <= max_val) ++bits;
    return bits;
}

constexpr int field_width = bits_for(255);   // 8 — compile time guaranteed
// int n = readInput(); bits_for(n);         // ERROR: n is not a constant expression

consteval lambdas are also supported in C++20 and follow the same rules:

cpp
// C++20 — consteval lambda
auto make_mask = [](int bits) consteval {
    return (1 << bits) - 1;
};

constexpr int mask4 = make_mask(4);   // 0b1111 = 15
static_assert(mask4 == 15);

constinit — guaranteed safe initialization

constinit solves the static initialization order fiasco for mutable globals. It requires the initializer to be a constant expression, eliminating cross-TU initialization dependencies, while still allowing the variable to be modified later:

cpp
// C++20 — constinit for mutable globals
constinit int request_count = 0;        // safe: no SIOF, still writable
constinit thread_local int depth = 0;   // per-thread, constant-initialized

void handle_request() {
    ++request_count;   // fine — constinit does not mean const
}

// constinit with a non-constant initializer is a hard error:
// int compute_default();
// constinit int bad = compute_default();   // ERROR at compile time

The three storage-class specifiers form a spectrum: constexpr = immutable + compile-time value; constinit = mutable + compile-time initializer; const = immutable + potentially runtime value.


Compile-time string processing

C++20's constexpr std::string and std::string_view enable non-trivial string manipulation at compile time. This is useful for compile-time parsing, format string validation, and generating string tables without runtime overhead.

cpp
// C++20 — compile-time string manipulation
#include <string>
#include <string_view>

constexpr bool starts_with(std::string_view s, std::string_view prefix) {
    return s.size() >= prefix.size() &&
           s.substr(0, prefix.size()) == prefix;
}

static_assert(starts_with("constexpr", "const"));
static_assert(!starts_with("volatile", "const"));

More complex processing — such as splitting, trimming, or validating format strings — is now feasible with std::string at compile time:

cpp
// C++20 — compile-time token count
#include <string>

constexpr int count_tokens(std::string_view s, char delim) {
    if (s.empty()) return 0;
    int count = 1;
    for (char c : s) if (c == delim) ++count;
    return count;
}

static_assert(count_tokens("a,b,c,d", ',') == 4);
static_assert(count_tokens("", ',') == 0);

Format string validation is a key real-world application — validate printf-style or custom format strings at compile time to turn malformed inputs into build errors rather than runtime crashes:

cpp
// C++20 — compile-time format string validator (simplified)
constexpr bool valid_format(std::string_view fmt) {
    int open = 0;
    for (std::size_t i = 0; i < fmt.size(); ++i) {
        if (fmt[i] == '{') ++open;
        else if (fmt[i] == '}') {
            if (open == 0) return false;   // unmatched '}'
            --open;
        }
    }
    return open == 0;   // all '{' must be closed
}

static_assert(valid_format("hello {} world {}"));
static_assert(!valid_format("bad } format"));

The std::string guarantee only applies to allocations that do not escape the constant expression — for returning compile-time strings, use std::array<char, N> or std::string_view pointing to a string literal.


See Also

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