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
- 2.
constexpr - 3.
consteval(C++20) - 4.
constinit(C++20) - 5.
if constexpr(C++17) - 6.
constexprContainers (C++20) - 7.
static_assert
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:
- Zero runtime cost — values known at compile time.
- Compile-time error catching — out-of-bounds, division by zero.
- 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
- Body must be evaluatable at compile time. C++14 relaxed this — loops, multiple statements, local mutation all OK.
- Args must be constant expressions for compile-time evaluation.
- 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)
consteval— immediate 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:
- Type-safe formatting —
std::formatusesconstevalfor compile-time format string checks. - Generating embedded constants — lookup tables, hash codes, IDs.
- Refusing runtime use of functions meant only for compile time.
- Compile-time-or-bust guarantee → strictly stronger than
constexpr.
4. constinit (C++20)
constinit— initializer must be compile-time. Variable itself isn't necessarilyconstafterward.
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 →
counterready 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
constinitis 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-timeif. 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 whenT = std::string. - → modern replacement for
enable_ifchains.
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::vectorallocates.constexprallocations must be freed within the same constant expression — no dangling allocations. - Correct pattern for compile-time results —
std::array, or compute intostd::vectorand 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::stringetc. work inconstexprcontexts (loops inconstexprfunctions) — 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:
- Validating template parameters ("T must be arithmetic").
- Asserting platform invariants — size, alignment, endianness.
- Pinning down constants that must hold across refactors.