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++11constexpr 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++:
| Keyword | Introduced | Meaning |
|---|---|---|
constexpr | C++11 | Variable: compile-time constant. Function: may evaluate at compile time. |
consteval | C++20 | Function: must evaluate at compile time (immediate function). |
constinit | C++20 | Variable: 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:
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 argumentFor globals and header-level constants, prefer inline constexpr (C++17) to avoid ODR violations across translation units:
// constants.hpp
inline constexpr int max_connections = 256; // C++17 — one definition, multiple TUsBefore 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:
// 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:
// 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 runtimeTo 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:
// 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 costconstexpr classes
Any class with constexpr constructors and member functions can participate in constant expressions. C++20 extended this to cover destructors and virtual functions:
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:
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 timeThis 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:
#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:
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:
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 initializedThe 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:
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
constexpr int square(int n) { return n * n; }
int x = readInput();
int r = square(x); // runs at runtime — no compile-time evaluationAssign 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:
constexpr std::array<int, 4> arr{10, 20, 30, 40};
// static_assert(arr[10] == 0); // compile error: out of bounds — not UB silently ignoredAt 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:
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.