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.
if consteval (C++23)
C++23 introduced if consteval (P1938R3) as a dedicated language construct for branching on evaluation context β replacing the error-prone std::is_constant_evaluated() pattern.
The problem with std::is_constant_evaluated()
std::is_constant_evaluated() has a subtle trap: combining it with if constexpr is always a bug.
// WRONG β always true: if constexpr forces the condition to be
// evaluated at compile time, so is_constant_evaluated() returns
// true unconditionally. The runtime branch is dead code.
constexpr int broken(int n) {
if constexpr (std::is_constant_evaluated()) { // warning: tautology
return n * n;
} else {
return n * n; // never reached
}
}GCC warns about this, but the trap is non-obvious and widely encountered. There is a second, separate problem: is_constant_evaluated() cannot be used to call a consteval function conditionally. The following is ill-formed even though the call only occurs on the compile-time path:
consteval int f(int i) { return i; }
constexpr int g(int i) {
if (std::is_constant_evaluated()) {
return f(i) + 1; // error: not in an immediate function context
}
return 42;
}How if consteval fixes both problems
if consteval is a language keyword, not a library call. It requires braces (mandatory, unlike regular if), takes no condition, and promotes its body to an immediate function context β making consteval calls legal inside it.
consteval int f(int i) { return i; }
constexpr int g(int i) {
if consteval {
return f(i) + 1; // OK: immediate function context
} else {
return 42;
}
}
constexpr auto a = g(5); // compile-time: f(5) + 1 == 6
int b = g(5); // runtime: 42There is no boolean expression to misplace, so the if constexpr trap cannot occur. if consteval always runs the first block during constant evaluation and the second block at runtime.
Negated form
constexpr void log_debug(const char* msg) {
if !consteval {
// Only executes at runtime β safe to call runtime-only APIs here.
std::fputs(msg, stderr);
}
// At compile time this function is a no-op.
}Note the spelling: if !consteval, not if (!consteval). No parentheses.
Dual-path dispatch pattern
#include <bit>
#include <cstdint>
consteval uint32_t popcount_portable(uint32_t x) {
int count = 0;
while (x) { count += (x & 1); x >>= 1; }
return count;
}
// Single function, two evaluation strategies.
constexpr uint32_t popcount(uint32_t x) {
if consteval {
return popcount_portable(x); // safe, portable loop
} else {
return std::popcount(x); // maps to POPCNT on x86 at runtime
}
}
constexpr uint32_t k = popcount(0b1101'0110u); // compile time: 5
static_assert(k == 5);Compiler support: GCC 12+, Clang 14+, MSVC 19.33+. Feature-test macro: __cpp_if_consteval.
Transient Heap Allocation in Constant Expressions (C++20)
C++20 permits new and delete inside constant expressions, and by extension makes std::vector and std::string usable inside constexpr functions. The key constraint is transience: every allocation made during a constant expression must be freed before evaluation completes. The memory exists only inside the compiler's abstract machine β it never reaches the program's runtime heap.
#include <vector>
#include <numeric>
constexpr int sum_n(int n) {
std::vector<int> v(n);
std::iota(v.begin(), v.end(), 1); // 1, 2, ..., n
int total = 0;
for (int x : v) total += x;
return total; // v is destroyed here β allocation is freed
}
static_assert(sum_n(10) == 55); // entire computation at compile timeThe vector is created and destroyed within the constant expression. The result (55) is a plain int with no heap lifetime.
What "transient" means in practice
An allocation is transient when it does not outlive the constant expression that created it. This has two important consequences:
You cannot store a constexpr std::vector or constexpr std::string as a namespace-scope variable. The allocation would need to persist into runtime, which is not yet permitted:
// ILL-FORMED: the vector's internal buffer cannot survive
// from compile time into runtime.
constexpr std::vector<int> kData = {1, 2, 3}; // error
// OK: the computation happens inside the constexpr function,
// and only the scalar result escapes.
constexpr int kSize = []() {
std::vector<int> v = {1, 2, 3};
return static_cast<int>(v.size());
}(); // kSize == 3Workaround β use std::array to capture results. Run the computation inside a constexpr function that returns a fixed-size array:
#include <array>
#include <vector>
#include <string>
#include <string_view>
constexpr std::size_t longest_word_size(std::string_view s) {
// std::string and std::vector are fine here β they're transient.
std::vector<std::string_view> words;
std::size_t start = 0;
for (std::size_t i = 0; i <= s.size(); ++i) {
if (i == s.size() || s[i] == ' ') {
if (i > start) words.push_back(s.substr(start, i - start));
start = i + 1;
}
}
std::size_t best = 0;
for (auto w : words) if (w.size() > best) best = w.size();
return best;
}
// Run the computation twice: once to learn the size, once to fill.
constexpr auto kWord = longest_word_size("hello world programming");
static_assert(kWord == 11); // "programming"Prerequisites in C++20
Four features collectively enable std::vector/std::string in constant expressions: constexpr destructors, constexpr new/delete, constexpr placement-new, and constexpr try/catch (for exception-safe containers). All four landed in C++20.
constexpr Algorithms (C++20)
C++20 marked more than 100 <algorithm> entries as constexpr, including std::sort, std::find, std::count, std::copy, std::fill, std::max_element, and the full suite of comparison and partitioning algorithms. Combined with constexpr std::array, this enables build-time data tables with zero runtime cost.
Compile-time sorted lookup table
#include <algorithm>
#include <array>
struct Entry {
int key;
const char* name;
constexpr bool operator<(const Entry& o) const { return key < o.key; }
};
// Unsorted at the source β sorted by the compiler.
constexpr std::array<Entry, 5> kTable = []() {
std::array<Entry, 5> t = { {
{42, "answer"},
{ 7, "lucky"},
{99, "max"},
{ 1, "one"},
{13, "baker"},
} };
std::sort(t.begin(), t.end()); // constexpr in C++20
return t;
}();
static_assert(kTable[0].key == 1);
static_assert(kTable[4].key == 99);
// Binary search at runtime β on a table sorted at compile time.
const Entry* lookup(int key) {
auto it = std::lower_bound(kTable.begin(), kTable.end(),
Entry{key, nullptr});
if (it != kTable.end() && it->key == key) return &*it;
return nullptr;
}The lambda runs at compile time because its result initializes a constexpr variable. std::sort rearranges the elements inside the constant expression; the sorted array is embedded directly into the binary.
Common constexpr algorithms and their C++20 status
| Algorithm | constexpr since |
|---|---|
std::sort, std::stable_sort | C++20 |
std::find, std::find_if | C++20 |
std::count, std::count_if | C++20 |
std::copy, std::fill | C++20 |
std::max_element, std::min_element | C++20 |
std::lower_bound, std::upper_bound | C++20 |
std::equal, std::mismatch | C++20 |
std::reverse, std::rotate | C++20 |
std::ranges equivalents are also constexpr in C++20. Algorithms that interact with I/O or random-number generation are excluded.
constinit In Depth (C++20)
The overview section introduced constinit briefly. This section covers the motivation and the distinctions that matter in practice.
The static-initialization-order fiasco
When a program has multiple translation units with namespace-scope variables whose initializers depend on each other, the order of dynamic initialization across those units is unspecified. A global Widget g_widget = make_widget() may silently run before the dependencies make_widget() needs are initialized. The result is undefined behavior that only manifests in certain link orders.
constinit eliminates this class of bugs by asserting that the variable's initializer is a constant expression β evaluated before any dynamic initialization runs:
constexpr int compute(int v) { return v * v * v; }
constinit int g_limit = compute(10); // constant initialization: g_limit == 1000
// constinit int g_other = g_limit; // error: g_limit is not a constant expressionIf the initializer cannot be evaluated at compile time, the declaration is ill-formed β a compile error, not silent undefined behavior.
constinit vs constexpr for variables
| Property | constexpr variable | constinit variable |
|---|---|---|
| Initialization | Constant expression (required) | Constant expression (required) |
| Immutable after init | Yes β implicitly const | No β can be modified at runtime |
| Usable in constant expressions | Yes | No |
| Applies to local auto variables | Yes | No β only static/thread_local |
Implies const | Yes | No |
constexpr int kMax = 100; // const; can be used in array bounds, templates
constinit int g_count = 0; // not const; can be incremented at runtime
g_count += 1; // OK: constinit does not mean immutable
// std::array<int, g_count> arr; // error: g_count is not a constant expressionconstinit on thread_local
constinit has a second use: telling the compiler that a thread_local variable is already statically initialized, so no hidden guard variable is needed on each access:
extern thread_local constinit int tls_counter;
int read_counter() {
return tls_counter; // no guard variable check β guaranteed constant init
}Without constinit, the compiler must emit a per-access check to handle the case where the thread_local has not yet been initialized for this thread. constinit eliminates that overhead when the initializer is a compile-time constant.
Cannot be combined with constexpr
// constexpr constinit int x = 0; // error: ill-formed
constinit const int y = 0; // OK but redundant β prefer constexprconstexpr and constinit cannot appear together. If you need a constant that is both immutable and usable in constant expressions, use constexpr. Use constinit only when you need a variable that has guaranteed compile-time initialization but must remain mutable at runtime.
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