Skip to content
C++
C++11 / C++14 / C++17 / C++20Intermediate

constexpr and consteval — Compile-Time Computation

constexprmarks a function or variable as eligible for evaluation at compile time. When the compiler can evaluate a constexpr function with all constant inputs, it replaces the call with the result — no runtime overhead, no function call, just a literal baked into the binary. If the inputs are not constant, the function falls back to being called at runtime like any other function. Each version of C++ has relaxed the restrictions, making constexpr progressively more powerful. C++20's consteval goes further — it requires compile-time evaluation and is an error otherwise.

constexpr variables

A constexpr variable must be initialized with a constant expression and is implicitly const. It can be used wherever a compile-time constant is required — as a template argument, array size, case label, etc. Prefer it over #define and over const for values that are genuinely compile-time constants.

// constexpr variable — must be initialized with a constant expression:
constexpr int MAX_BUFFER = 4096;
constexpr double PI = 3.14159265358979;
constexpr auto GREETING = "hello";   // const char*

// Use as array size, template argument, or case label:
int buffer[MAX_BUFFER];              // ok — constexpr is a constant expression
std::array<int, MAX_BUFFER> arr;     // ok
switch (x) { case MAX_BUFFER: ... }  // ok

// const is NOT the same as constexpr:
const int a = 42;        // might be constexpr, might not — depends on the initializer
const int b = rand();    // definitely not constexpr (runtime)
constexpr int c = 42;    // definitely constexpr
// constexpr int d = rand(); // ERROR — rand() is not a constant expression

constexpr functions

A constexpr function can be called at compile time (if given constant arguments) or at runtime (if given runtime values). C++11 restricted them to a single return statement; C++14 removed this restriction — local variables, loops, and branches are all allowed. C++17 allowed constexpr lambdas and if constexpr. C++20 allows virtual functions, dynamic_cast, and even std::string and std::vector in constexpr contexts.

// C++14 constexpr — loops and local variables allowed:
constexpr int factorial(int n) {
    int result = 1;
    for (int i = 2; i <= n; ++i)
        result *= i;
    return result;
}

// Compile-time evaluation when given a constant:
constexpr int f5 = factorial(5);   // evaluated at compile time: 120
static_assert(f5 == 120);          // compile-time check

// Runtime fallback when given a runtime value:
int n;
std::cin >> n;
int fn = factorial(n);   // called at runtime — that's fine

// Compile-time lookup table:
constexpr std::array<int, 8> make_powers_of_two() {
    std::array<int, 8> table{};
    for (int i = 0; i < 8; ++i) table[i] = 1 << i;
    return table;
}
constexpr auto POW2 = make_powers_of_two();
// POW2 is {1, 2, 4, 8, 16, 32, 64, 128} — computed at compile time

if constexpr (C++17) — compile-time branching in templates

Inside a template, if constexpr lets you pick a branch at compile time. The discarded branch is not even instantiated — it can contain code that would not compile for this type. This replaced many complex SFINAE patterns.

template<typename T>
void print(const T& value) {
    if constexpr (std::is_integral_v<T>) {
        std::cout << "integer: " << value;
    } else if constexpr (std::is_floating_point_v<T>) {
        std::cout << std::fixed << "float: " << value;
    } else {
        std::cout << "other: " << value;
    }
    // The branches not taken are not compiled for this T.
    // They can contain code that would be ill-formed for other types.
}

consteval (C++20) — immediate functions

consteval declares an immediate function — one that must be evaluated at compile time. Calling it with a runtime argument is a compile error. Use it when you want a guarantee that something is computed at compile time, not just an opportunity.

// constexpr — evaluated at compile time IF arguments are constant:
constexpr int square(int x) { return x * x; }
int a = 5;
int b = square(a);   // runtime — a is not a constant expression

// consteval — MUST be evaluated at compile time:
consteval int square_ct(int x) { return x * x; }
constexpr int c = square_ct(5);  // ok — 5 is a constant expression
// int d = square_ct(a);         // ERROR — a is not a constant expression

// Practical use: compile-time string hashing, format string validation:
consteval std::size_t hash(std::string_view s) {
    std::size_t h = 0;
    for (char c : s) h = h * 31 + c;
    return h;
}
constexpr std::size_t h1 = hash("hello");   // computed at compile time — no runtime cost

constinit (C++20) — guaranteed static initialization

constinit asserts that a variable with static or thread storage duration is initialized with a constant expression — but unlike constexpr, the variable can still be modified at runtime. It prevents the static initialization order fiasco by guaranteeing initialization happens before any runtime code.

// constexpr — const, computed at compile time, cannot change:
constexpr int MAX = 100;

// constinit — computed at compile time, but CAN change at runtime:
constinit int counter = 0;   // zero-initialized before any code runs
void increment() { ++counter; }   // runtime modification is fine

// Prevents the static init order fiasco:
// Without constinit, static variables in different TUs might init in wrong order.
// With constinit, initialization is guaranteed before any runtime code.

Key rules to remember

constexpr functions are evaluated at compile time only if all arguments are constant expressions

If any argument is a runtime value, the function is called at runtime like a normal function. This dual-mode behavior is often desirable.

consteval forces compile-time evaluation — a runtime argument is a compile error

Use consteval when you want a guarantee, not just an opportunity. Format string validators and compile-time hashers are typical use cases.

Use constexpr variables instead of #define for numeric constants

#define has no type, no scope, and no debugger visibility. constexpr has all three and participates in overload resolution normally.

C++20 allows std::string and std::vector in constexpr functions

As long as all dynamic memory allocation is released within the constant expression evaluation, constexpr functions can use heap-allocating types.

Sign in to track progress