Constant Expressions
Expressions evaluated at compile time, enabling array bounds, template arguments, enum values, alignas, and zero-overhead constexpr abstractions.
Constant Expressionsince C++98A constant expression is an expression whose value the compiler can fully determine at translation time, making it usable wherever the language requires a compile-time constant β array bounds, non-type template arguments, enumerator values, alignas operands, and static_assert conditions.
Overview
Constant expressions have existed since C++98, but the rules governing them have been progressively liberalised across every major standard revision.
C++98/03 restricted compile-time constants to integral and enumeration types. A const int initialized from a literal or another such constant qualified; const double did not. Array bounds and case labels required these integral constant expressions.
C++11 introduced constexpr, a keyword that explicitly marks variables and functions as being eligible for compile-time evaluation. It also generalised the notion of a literal type β the category of types whose values can exist in a constant expression. C++11 constexpr functions were severely restricted: a single return statement, no local variables, no loops.
C++14 lifted most of those restrictions. constexpr functions can now contain local variables, loops, if/switch, and multiple return statements. Member functions are no longer implicitly const. This made it practical to port real algorithms to compile-time evaluation.
C++17 added if constexpr for compile-time branch selection, made lambdas implicitly constexpr when their body qualifies, and allowed constexpr on range-based for loops and other constructs. std::array, std::tuple, and most <algorithm> entries became constexpr.
C++20 introduced two new specifiers and one query function:
constevalβ forces a function to be an immediate function: every call must produce a constant expression; runtime calls are an error.constinitβ asserts that a variable has constant initialization without implyingconst; useful for globals that must not suffer static-initialization-order issues.std::is_constant_evaluated()β returnstruewhen called inside a manifestly constant-evaluated context, allowing a single function body to branch between a compile-time-friendly path and an optimised runtime path.
C++23 further relaxed constexpr to allow goto, labels, static local variables (with constant initialization), and thread_local variables in constexpr functions.
Syntax
// constexpr variable β must be initialized from a constant expression
constexpr int cache_line = 64; // C++11
constexpr double inv_sqrt2 = 0.70710678118654752440; // C++11
// constexpr function β may be evaluated at compile or run time
constexpr int square(int x) { return x * x; } // C++11 (single return only)
// C++14: full function body
constexpr int factorial(int n) { // C++14
int result = 1;
for (int i = 2; i <= n; ++i) result *= i;
return result;
}
// consteval β guaranteed compile-time; runtime call = error
consteval int pow2(int exp) { // C++20
int v = 1;
for (int i = 0; i < exp; ++i) v <<= 1;
return v;
}
// constinit β constant initialization, mutable at runtime
constinit int g_flags = pow2(4); // C++20; g_flags == 16, not const
// if constexpr β compile-time branch elimination
template <typename T>
auto to_string_or_value(T v) {
if constexpr (std::is_same_v<T, std::string>) { // C++17
return v;
} else {
return std::to_string(v);
}
}Literal Types
Only literal types can participate in constant expressions. The set includes:
- All scalar types (
int,double, pointers, enumerations, β¦) - References to literal types
- Arrays of literal types
- Class types where: the destructor is
constexpr, all non-static data members and bases are of non-volatile literal types, and at least one of: aconstexprconstructor exists, it is an aggregate, or it is a union with at least one literal-type variant member.
Since C++20, std::string, std::vector, and std::optional are literal types in constant expressions via the mechanism of transient allocation β heap allocations performed inside a constant expression that are fully freed before the expression completes are allowed.
Examples
Non-type template arguments
template <std::size_t N>
struct FixedBuffer {
std::array<std::byte, N> data;
};
constexpr std::size_t kPageSize = 4096;
FixedBuffer<kPageSize> page_buf; // OK: kPageSize is a constant expression
FixedBuffer<kPageSize * 2> double_buf; // OK: arithmetic on constant expressionsCompile-time lookup table
constexpr std::array<int, 8> build_powers_of_two() { // C++14 + C++17 constexpr array
std::array<int, 8> t{};
for (int i = 0; i < 8; ++i) t[i] = 1 << i;
return t;
}
constexpr auto kPow2Table = build_powers_of_two(); // evaluated at compile time
static_assert(kPow2Table[7] == 128);Runtime/compile-time dual path with std::is_constant_evaluated
#include <cmath>
#include <type_traits>
constexpr double fast_sqrt(double x) {
if (std::is_constant_evaluated()) { // C++20
// Newton-Raphson; no libm at compile time
double r = x;
for (int i = 0; i < 32; ++i) r = 0.5 * (r + x / r);
return r;
}
return std::sqrt(x); // hardware-accelerated at runtime
}
constexpr double kSqrt2 = fast_sqrt(2.0); // compile-time Newton pathconsteval for guaranteed compile-time execution
consteval std::uint32_t fnv1a_32(const char* s, std::size_t len) {
std::uint32_t h = 2166136261u;
for (std::size_t i = 0; i < len; ++i)
h = (h ^ static_cast<std::uint8_t>(s[i])) * 16777619u;
return h;
}
// String literal hashed at compile time; no runtime cost
constexpr auto kEventId = fnv1a_32("player.death", 12); // C++20
// fnv1a_32(runtime_string, n); // error: consteval requires constant expressionBest Practices
Prefer constexpr over const for compile-time values. const on an integral variable may produce a constant expression if the initializer qualifies, but constexpr makes the intent explicit and is enforced by the compiler. A constexpr variable is always const and its initializer is always a constant expression.
Use consteval to enforce call-site purity. When a function's only legitimate use is compile-time (hash functions, unit conversion factors, bit-manipulation tables), consteval prevents accidental runtime calls that silently fall back to runtime evaluation.
Use constinit for safely initialized globals. It eliminates static-initialization-order fiasco for global state that is mutable at runtime but must have deterministic initial values. Without constinit, a subtly non-constant initializer silently becomes a runtime initialization, which can race or depend on ordering.
Sink complexity into constexpr helper functions rather than macros. Constant-expression arithmetic in constexpr functions is type-safe, debuggable, and obeys scope rules. The old #define SQUARE(x) ((x)*(x)) pattern has no advantages over a constexpr function.
Common Pitfalls
const is not constexpr. A const double initialized from a non-constant is not a constant expression:
const double pi = std::acos(-1.0); // runtime-initialized; NOT a constant expression
constexpr double pi_ce = 3.14159265358979323846; // constant expressionconstexpr functions can degrade to runtime. If all arguments are runtime values, a constexpr function executes at runtime with no diagnostic. Only consteval guarantees compile-time evaluation.
C++11's single-return restriction. If you must support C++11, constexpr function bodies may contain only a single return (plus static_assert and typedef). The relaxations β loops, local variables, multiple returns β require C++14. Target appropriately.
Floating-point constant expressions are permitted but implementation-defined in precision. Compilers are not required to use the same rounding as IEEE 754 runtime arithmetic. Avoid comparing constexpr floats with == and be aware that a static_assert on a constexpr double result may behave differently than the equivalent runtime check.
Transient heap allocation in constant expressions requires C++20. Code like constexpr std::vector<int> v = {1, 2, 3}; is ill-formed before C++20. In C++17 and earlier, only types with no dynamic storage (trivial or explicitly constexpr-constructed) qualify.
See Also
reference/language/const-correctnessβconstsemantics and the relationship to constant expressionsreference/language/aggregate-initializationβ literal class types and their role in constexpr contextsreference/language/class-templateβ non-type template parameters that require constant expressions