C++ `constexpr` and Compile-Time Programming
- 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
C++ has three keywords for controlling when a value or function is evaluated: at compile time, at 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. |
The benefit of compile-time evaluation:
- Zero runtime cost for values known at compile time.
- Catches errors at compile time (out-of-bounds, division by zero, etc.).
- Usable in non-type template parameters and array sizes.
2. constexpr
constexpr (C++11) declares that a function or variable can be evaluated at compile time. Whether it actually is depends on the 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
- The function body must consist of statements that can be evaluated at compile time. C++14 relaxed this dramatically (loops, multiple statements, mutation of local variables are all allowed).
- Arguments must be constant expressions for compile-time evaluation.
- The function does not have to be evaluated at compile time — it depends on the call site.
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 is a stronger guarantee than const: it requires compile-time evaluation. Use constexpr whenever the value can be known at compile time.
3. consteval (C++20)
consteval declares an immediate function — one that must be evaluated at compile time. There is no runtime version.
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 consteval when you want 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 a function that should never be called at runtime.
The compile-time-or-bust guarantee makes consteval strictly stronger than constexpr.
4. constinit (C++20)
constinit requires that a variable's initializer be evaluated at compile time, but the variable itself isn't necessarily const afterward.
constinit int counter = 0; // initialized at compile time, mutable at runtime
void increment() {
counter++; // OK: not const
}
The main use case is avoiding the static initialization order fiasco. By forcing compile-time initialization, you guarantee counter is ready before any other TU's runtime initializers fire.
// 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 purely about initialization; the variable is normal, mutable storage afterward. Compare:
| 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 is a compile-time if. The compiler discards the not-taken branch entirely — including its type-check requirements. 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 if constexpr, you'd need SFINAE or tag dispatch to express the same logic. The not-taken branches are not type-checked in their original context — the call to std::to_string(x) is fine even when T is std::string (because the branch is discarded).
This is why if constexpr is the modern replacement for enable_if chains.
6. constexpr Containers (C++20)
C++20 made many standard library 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);
Wait — that doesn't actually work directly. std::vector allocates memory, and constexpr allocations must be freed within the same constant expression (no dangling allocations leaving the constant expression).
The correct pattern for compile-time results: use std::array, or compute into a 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 can use them) — they just can't survive past the constant expression.
7. static_assert
static_assert(cond, "msg") is a compile-time assertion. The 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;
}
Since C++17, the message is optional:
static_assert(sizeof(void*) == 8); // no message — pretty diagnostic from the expression
static_assert is the standard tool for:
- Validating template parameters ("T must be arithmetic").
- Asserting platform invariants (size, alignment, endianness).
- Pinning down constants that must hold across refactors.