C++ can run ordinary code inside the compiler. Used well, that gives you three
things: work that costs nothing at run time (tables, parsed constants, hashes),
values usable where the language demands constants (array bounds, template
arguments, case labels), and — the underrated one — a compiler that
refuses to accept undefined behaviour in anything it evaluates.
Four keywords, four different promises:
| keyword | applies to | promise |
|---|---|---|
constexpr |
variables | the value is computed at compile time and the variable is const |
constexpr |
functions | the function may run at compile time |
consteval |
functions | every call must run at compile time |
constinit |
variables | initialised at compile time, but not const |
For header definitions, distinguish functions from variables: a constexpr
function is implicitly inline, but an ordinary namespace-scope constexpr
variable is not. Use inline constexpr for one shared constant object across
translation units; a static constexpr data member is already implicitly inline.
The ODR section in project building explains the
definition and linkage rules.
constexpr variables, consteval calls and constinit initialisers are finished inside the compiler; their results are bytes in the binary. Ordinary globals still run constructors at start-up, in an order that's unspecified across translation units.1. constexpr functions are permission, not a command
A constexpr function is an ordinary function that the compiler is allowed
to evaluate. Whether it does depends on the call site:
#include <print>
constexpr int factorial(int n) {
int r = 1;
for (int i = 2; i <= n; ++i) r *= i;
return r;
}
int main(int argc, char**) {
constexpr int a = factorial(5); // must be a constant: evaluated by the compiler
int arr[factorial(3)]{}; // array bound: a constant expression is required
int b = factorial(argc + 2); // argc is run-time: an ordinary call
std::println("{} {} {}", a, sizeof arr / sizeof arr[0], b);
}Constant evaluation is required (the language says "manifestly
constant-evaluated") in: constexpr and constinit variable initialisers,
template arguments, array bounds, case labels, static_assert, if constexpr
conditions — and, less obviously, initialisers of const integral variables
and of variables with static storage duration, which the compiler tries to
constant-initialise first. Everywhere else, a call runs at run time (the
optimiser may still fold it, but that's invisible to the language).
2. What you can do at compile time
Almost everything you'd write in a normal function:
- locals, loops,
if/switch, recursion, mutation of locals; - calls to other
constexprfunctions, including most of the standard library (std::array,std::string_view,std::optional, algorithms…); - transient allocation (C++20):
new/delete,std::vector,std::string— as long as everything allocated is freed before the evaluation ends; - virtual calls,
dynamic_cast,typeid(C++20); - exceptions (C++26):
throw,try/catch, as long as nothing escapes the evaluation. GCC 16 implements this; Clang 23 doesn't yet.
#include <algorithm>
#include <print>
#include <stdexcept>
#include <vector>
// A sorted copy of the input's distinct values, as a count: uses a vector
// during constant evaluation and frees it before returning.
constexpr int distinct(std::initializer_list<int> xs) {
std::vector<int> v(xs);
std::ranges::sort(v);
auto [first, last] = std::ranges::unique(v);
v.erase(first, last);
return static_cast<int>(v.size());
}
constexpr int digit(char c) {
if (c < '0' || c > '9') throw std::invalid_argument("not a digit");
return c - '0';
}
constexpr int digit_or(char c, int fallback) {
try {
return digit(c);
} catch (const std::invalid_argument&) {
return fallback;
}
}
static_assert(distinct({3, 1, 3, 2, 1}) == 3);
static_assert(digit_or('7', -1) == 7);
static_assert(digit_or('x', -1) == -1);
int main() { std::println("all checked by the compiler"); }What you can't do: keep an allocation alive past the evaluation
(constexpr std::vector<int> v{1, 2}; at namespace scope is an error — use
std::array), reinterpret_cast (use std::bit_cast), read or write a
mutable static local, call non-constexpr functions, or let an exception
escape.
3. Undefined behaviour is a compile error
During constant evaluation the compiler is required to diagnose undefined
behaviour: out-of-bounds access, signed overflow, null dereference,
reading uninitialised memory, use after free. That turns static_assert into
a sanitizer that runs on every build:
#include <climits>
constexpr int sum_first(const int* p, int n) {
int total = 0;
for (int i = 0; i <= n; ++i) // off by one: reads p[n]
total += p[i];
return total;
}
constexpr int data[4]{1, 2, 3, 4};
static_assert(sum_first(data, 4) == 10); // error: read past the end of dataThe same function called at run time compiles fine and silently reads past
the array. constexpr doesn't check run-time calls — it only guarantees the
compiler won't constant-evaluate UB. A cheap habit: give your pure helper
functions a few static_assert tests next to their definition.
4. consteval: compile time only
A consteval (immediate) function must be evaluated at compile time at every
call. Use it when a run-time call would be a mistake — building a table,
validating a format string, hashing a literal:
#include <print>
consteval unsigned fnv1a(const char* s) {
unsigned h = 2166136261u;
for (; *s; ++s) h = (h ^ static_cast<unsigned char>(*s)) * 16777619u;
return h;
}
int main() {
constexpr unsigned id = fnv1a("player.jump"); // computed by the compiler
unsigned copy = fnv1a("player.jump"); // still compile time; stored at run time
std::println("{:#x} {}", id, id == copy);
}fnv1a(argv[0]) would not compile. Inside a plain constexpr function, a
parameter is never a constant expression, so it can't be passed to a
consteval function either — unless you're inside if consteval:
#include <print>
consteval int square_ct(int x) { return x * x; }
constexpr int square(int x) {
if consteval {
return square_ct(x); // only during constant evaluation: x is a constant here
} else {
return x * x; // run time
}
}
int main(int argc, char**) {
constexpr int a = square(12);
std::println("{} {}", a, square(argc));
}The std::is_constant_evaluated() trap
std::is_constant_evaluated() (C++20) asks the same question as
if consteval, but as an ordinary function call. Two traps:
if constexpr (std::is_constant_evaluated())is always true — the condition ofif constexpris itself constant-evaluated. Useif consteval.- It reports manifest constant evaluation, not "will the optimiser fold
this".
int b = f();gets the run-time branch;static int d = f();andconst int c = f();get the compile-time branch, because the compiler must try to constant-initialise them.
5. constinit and the initialisation-order fiasco
Globals in different translation units are dynamically initialised in an
unspecified order, so one global's constructor can use another before it has
been constructed. constinit demands constant initialisation (a compile error
otherwise) — and constant-initialised objects exist before any dynamic
initialisation runs:
logger and config live in different files. If the linker happens to run logger's constructor first, it reads config before config's constructor has run. A constinit config is already initialised when the program is loaded.#include <array>
#include <print>
struct Config {
int verbosity;
std::array<int, 3> limits;
};
constinit Config config{1, {10, 20, 30}}; // in the binary's data section, ready at load
constinit thread_local int depth = 0; // also works for thread_local
int main() {
config.verbosity = 2; // unlike constexpr, constinit is not const
++depth;
std::println("{} {} {}", config.verbosity, config.limits[2], depth);
}6. Worked example: a compile-time CRC table and string switch
Three pieces of compile-time machinery working together:
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <print>
#include <string_view>
// 1. A table only the compiler may build.
consteval std::array<std::uint32_t, 256> make_crc_table() {
std::array<std::uint32_t, 256> table{};
for (std::uint32_t i = 0; i < 256; ++i) {
std::uint32_t c = i;
for (int bit = 0; bit < 8; ++bit)
c = (c & 1) ? (c >> 1) ^ 0xEDB88320u : c >> 1;
table[i] = c;
}
return table;
}
inline constexpr auto crc_table = make_crc_table(); // 1 KiB of read-only data, no code
// 2. A function usable at both times.
constexpr std::uint32_t crc32(std::string_view s) {
std::uint32_t crc = 0xFFFFFFFFu;
for (char ch : s) crc = crc_table[(crc ^ static_cast<unsigned char>(ch)) & 0xFF] ^ (crc >> 8);
return crc ^ 0xFFFFFFFFu;
}
static_assert(crc32("123456789") == 0xCBF43926u); // the standard check value
// 3. A string literal as a template argument (a "structural" class type).
template <std::size_t N>
struct fixed_string {
char chars[N]{};
constexpr fixed_string(const char (&s)[N]) { std::copy_n(s, N, chars); }
constexpr std::string_view view() const { return {chars, N - 1}; }
};
template <fixed_string S>
consteval std::uint32_t crc_of() { return crc32(S.view()); }
int command(std::string_view name) {
switch (crc32(name)) { // run time: hash the input once
case crc_of<"start">(): return 1; // compile time: constants
case crc_of<"stop">(): return 2;
default: return 0;
}
}
int main() { std::println("{} {} {}", command("start"), command("stop"), command("pause")); }make_crc_table is consteval, so no code for it exists in the binary — only these 1024 bytes in .rodata. crc_table[1] is the CRC step for byte value 1.Check it in the compiler explorer: make_crc_table produces no code at all —
only a 256-entry table in .rodata — and each case is a plain integer
compare. (Real code would still compare the strings after a hash match, since
different strings can share a hash.)
Faded example — your turn
Build a 256-entry table of bit counts at compile time, and use it in a
constexpr popcount for std::uint32_t (four table lookups). Which keyword
goes on the builder, and how do you prove the result with no run-time test?
/* ??? */ std::array<std::uint8_t, 256> make_bits_table() {
std::array<std::uint8_t, 256> t{};
for (int i = 1; i < 256; ++i) t[i] = /* ??? */;
return t;
}
inline constexpr auto bits = make_bits_table();
constexpr int popcount(std::uint32_t x) { /* four lookups */ }
static_assert(/* ??? */);Answer
consteval on the builder; t[i] = t[i >> 1] + (i & 1) (each entry reuses a
smaller one); popcount sums bits[x & 0xFF] + bits[(x >> 8) & 0xFF] + …;
and static_assert(popcount(0xF0F0'0001u) == 9) proves it at compile time.
std::popcount from <bit> is the production answer.
Summary
constexprfunction: may run at compile time; it does where a constant is required. Run-time calls are ordinary — and unchecked.consteval: must run at compile time. Bridge fromconstexprcode withif consteval, notstd::is_constant_evaluated()insideif constexpr.constinit: constant-initialised but mutable — the cure for initialisation-order bugs.- Allowed at compile time: loops, transient allocation, virtual calls, and in
C++26 exceptions. Not allowed: escaping allocations,
reinterpret_cast, mutable statics. - UB during constant evaluation is a compile error — use
static_asserttests as a free sanitizer.