Skip to content
C++
Language
since C++98
Intermediate

Constant Expressions

Expressions evaluated at compile time, enabling array bounds, template arguments, enum values, alignas, and zero-overhead constexpr abstractions.

Constant Expressionsince C++98

A 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 implying const; useful for globals that must not suffer static-initialization-order issues.
  • std::is_constant_evaluated() β€” returns true when 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

cpp
// 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: a constexpr constructor 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

cpp
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 expressions

Compile-time lookup table

cpp
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

cpp
#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 path

consteval for guaranteed compile-time execution

cpp
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 expression

Best 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:

cpp
const double pi = std::acos(-1.0);   // runtime-initialized; NOT a constant expression
constexpr double pi_ce = 3.14159265358979323846;  // constant expression

constexpr 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 β€” const semantics and the relationship to constant expressions
  • reference/language/aggregate-initialization β€” literal class types and their role in constexpr contexts
  • reference/language/class-template β€” non-type template parameters that require constant expressions