C++ `constexpr` and Compile-Time


  • Description: A note on constexpr (C++11+), consteval (C++20), constinit (C++20), if constexpr (C++17), static_assert, and compile-time evaluation rules
  • My Notion Note ID: K2A-B1-18
  • Created: 2019-08-15
  • Updated: 2026-02-28
  • License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io

Table of Contents


1. Compile-Time Programming Overview

  • 3 keywords for when evaluation happens — compile time, run time, or both.
Keyword Meaning
constexpr May be evaluated at compile time. Can also run at runtime.
consteval Must be evaluated at compile time. Pure compile-time function.
constinit Variable's initializer must run at compile time, but the variable itself isn't const.

Benefits of compile-time evaluation:

  1. Zero runtime cost — values known at compile time.
  2. Compile-time error catching — out-of-bounds, division by zero.
  3. Usable in non-type template params and array sizes.

2. constexpr

  • constexpr (C++11) — function/var may be evaluated at compile time. Actual evaluation depends on call context.
constexpr int square(int x) { return x * x; }

constexpr int a = square(5);     // compile-time: a = 25 (must be const expression)

int n = read_input();
int b = square(n);                // runtime: same function works dynamically too

static_assert(square(5) == 25);   // compile-time check

Rules for constexpr functions

  1. Body must be evaluatable at compile time. C++14 relaxed this — loops, multiple statements, local mutation all OK.
  2. Args must be constant expressions for compile-time evaluation.
  3. Not required to be evaluated at compile time — call site decides.

constexpr variables

constexpr int kBufferSize = 256;        // compile-time constant
constexpr double kPi = 3.14159;         // also OK
constexpr int squared = square(7);      // OK: square is constexpr

int n = 5;
constexpr int x = square(n);            // ERROR: n is not a constant expression

const vs constexpr

const int a = read_input();             // a is const, but not necessarily compile-time
constexpr int b = read_input();         // ERROR: read_input() isn't constexpr

const int c = 100;                      // c may or may not be compile-time
constexpr int d = 100;                  // d is definitely compile-time
  • constexpr > const — requires compile-time evaluation. Use whenever the value is compile-time-known.

3. consteval (C++20)

  • constevalimmediate function. Must be evaluated at compile time. No runtime form.
consteval int cube(int x) { return x * x * x; }

constexpr int a = cube(3);           // OK: 27, evaluated at compile time
int n = read_input();
// int b = cube(n);                   // ERROR: must be constant expression

static_assert(cube(4) == 64);

Use to enforce compile-time-only:

  1. Type-safe formattingstd::format uses consteval for compile-time format string checks.
  2. Generating embedded constants — lookup tables, hash codes, IDs.
  3. Refusing runtime use of functions meant only for compile time.
  • Compile-time-or-bust guarantee → strictly stronger than constexpr.

4. constinit (C++20)

  • constinitinitializer must be compile-time. Variable itself isn't necessarily const afterward.
constinit int counter = 0;            // initialized at compile time, mutable at runtime

void increment() {
    counter++;                         // OK: not const
}
  • Main use: avoid the static initialization order fiasco. Compile-time init → counter ready before any other TU's runtime initializers.
// Without constinit — static init order is unspecified
int slow_init = compute();           // dynamic init: order unspecified across TUs

// With constinit — guaranteed initialized before any dynamic init
constinit int fast_init = 42;        // dynamic-init-free
  • constinit is about initialization only. Storage is normal + mutable afterward.
Storage class Initializer After init
const int x = 5; runtime or compile-time read-only
constexpr int x = 5; must be compile-time read-only
constinit int x = 5; must be compile-time mutable

5. if constexpr (C++17)

  • if constexpr — compile-time if. Not-taken branch is discarded entirely (incl. type-checking). Essential in templates.
template <typename T>
auto serialize(const T& x) {
    if constexpr (std::is_arithmetic_v<T>) {
        return std::to_string(x);              // only compiled when T is numeric
    } else if constexpr (std::is_same_v<T, std::string>) {
        return x;                              // only compiled when T is string
    } else {
        return std::string{"?"};               // catch-all
    }
}
  • Without it → SFINAE or tag dispatch.
  • Discarded branches not type-checked in their original context — std::to_string(x) is fine even when T = std::string.
  • → modern replacement for enable_if chains.

6. constexpr Containers (C++20)

  • C++20 made many stdlib types constexpr-friendly:
#include <vector>
#include <string>
#include <algorithm>

constexpr std::vector<int> compute() {
    std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
    std::sort(v.begin(), v.end());
    return v;
}

constexpr auto sorted = compute();      // sorted at compile time!
static_assert(sorted[0] == 1);
  • But: std::vector allocates. constexpr allocations must be freed within the same constant expression — no dangling allocations.
  • Correct pattern for compile-time results — std::array, or compute into std::vector and copy out:
constexpr std::array<int, 5> first_five_primes = []() {
    std::array<int, 5> a = {2, 3, 5, 7, 11};
    return a;
}();
  • std::vector / std::string etc. work in constexpr contexts (loops in constexpr functions) — just can't survive past the constant expression.

7. static_assert

  • static_assert(cond, "msg") — compile-time assertion. Condition must be a constant expression.
static_assert(sizeof(int) >= 4, "int must be at least 4 bytes");

template <typename T>
struct Buffer {
    static_assert(std::is_trivially_copyable_v<T>,
                  "Buffer requires trivially copyable types");
    T data[64];
};

constexpr int compute() {
    static_assert(true, "always passes");    // OK in constexpr functions too
    return 42;
}
  • C++17 — message optional:
static_assert(sizeof(void*) == 8);   // no message — pretty diagnostic from the expression

Standard tool for:

  1. Validating template parameters ("T must be arithmetic").
  2. Asserting platform invariants — size, alignment, endianness.
  3. Pinning down constants that must hold across refactors.