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 expressionconstexpr 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 timeif 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 costconstinit (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.