C++ / FUNCTIONS
constexpr functions evaluated at compile time
Write constexpr functions, force evaluation during compilation, and tell which calls the compiler folds and which stay ordinary runtime calls.
What you will learn
- Force compile-time evaluation with constexpr variables, static_assert, or array bounds
- Read constexpr on a function as permission, not a guarantee of folding
- Use std::is_constant_evaluated() to prove which path a call actually took
- Explain why I/O, mutable globals, and UB push a call out of constant evaluation
Understanding constexpr functions evaluated at compile time
constexpr in front of a function is a permit, not an order. It says the body is written in the subset of C++ that the compiler's own evaluator can execute, so a call whose arguments are themselves constant expressions may be computed while the program is being translated and replaced by its result. The same function is still compiled to ordinary machine code as well, because nothing stops you from calling it with values that only exist at runtime. The mental model: the compiler carries a small C++ interpreter, and constexpr marks which functions it is allowed to feed to it.
What actually forces evaluation is the context, not the keyword. Initialising a constexpr variable, an array bound, a static_assert condition, a non-type template argument, a case label and an enumerator all require a constant expression, and there the compiler must evaluate the call or reject the program. Assign the same call to a plain int and the compiler is free either to fold it or to emit a real call; an unoptimised build usually emits the call. That is why "it compiled" is no evidence of compile-time evaluation, while a passing static_assert is.
The restrictions exist because constant evaluation happens with no process running: no live heap, no I/O, no mutable global state, and no undefined behaviour to paper over. Since C++14 the check is applied per evaluation rather than per declaration, so a body may contain a throw, a division that could overflow, or a call to a non-constexpr function, as long as the path taken during constant evaluation never reaches it. This is exactly why one call to a function can be a compile error while another call to the same function throws at runtime, and why C++20 added consteval for the case where you want a runtime call to be impossible rather than merely unlikely.
<iostream>
// constexpr is a permit: this body may run inside the compiler.
constexpr int pow_int(int base, int n) {
int result = 1;
for (int i = 0; i < n; ++i) {
result *= base;
}
return result;
}
int main() {
constexpr int size = pow_int(2, 5); // this context demands a constant expression
static_assert(size == 32); // proof that the compiler computed it
int table[size]{}; // array bounds accept only constant expressions
table[size - 1] = 9;
int n = 5; // an ordinary runtime variable
int later = pow_int(2, n); // same function, ordinary runtime call
std::cout << "size = " << size << '\n';
std::cout << "slot = " << table[size - 1] << '\n';
std::cout << "later = " << later << '\n';
}
Whether a constexpr function runs inside the compiler is decided by the call site's need for a constant expression, not by the keyword itself.
Worked examples
One function, two lives
Shows a single constexpr function taking a different branch depending on whether the compiler or the CPU is running it.
<iostream>
<type_traits> // std::is_constant_evaluated (C++20)
constexpr int tag(int n) {
if (std::is_constant_evaluated()) {
return n + 1000; // reached only inside the compiler
}
return n; // reached only by the generated machine code
}
int main() {
constexpr int folded = tag(7); // context requires a constant expression
int n = 7;
int executed = tag(n); // n is not a constant expression
std::cout << "folded = " << folded << '\n';
std::cout << "executed = " << executed << '\n';
}
Example explained
Line 1constexpr int folded = tag(7); cannot be resolved at runtime, so the compiler runs tag itself and is_constant_evaluated() reports true.
Line 2tag(n) with a plain int n can never be a constant expression, so the call is compiled into real code and takes the second return.
Line 3is_constant_evaluated() answers a language question, not an optimiser question: turning on -O2 does not change the printed values.
Line 4The two lines prove that the keyword grants permission while the call site chooses the outcome.
When constant evaluation refuses
Demonstrates that a precondition violation becomes a compile error in a constant context and a normal exception at runtime.
<iostream>
<stdexcept>
constexpr int checked_div(int a, int b) {
if (b == 0) {
throw std::domain_error("b == 0"); // legal in the body, unevaluable at compile time
}
return a / b;
}
int main() {
constexpr int ok = checked_div(84, 2);
static_assert(ok == 42);
std::cout << "ok = " << ok << '\n';
// constexpr int bad = checked_div(1, 0); // compile error: not a constant expression
int zero = 0;
try {
std::cout << checked_div(1, zero) << '\n';
} catch (const std::domain_error& e) {
std::cout << "runtime throw: " << e.what() << '\n';
}
}
Example explained
Line 1The throw statement is allowed inside a constexpr function; only evaluating it during translation is forbidden.
Line 2checked_div(84, 2) never reaches the throw, so the compiler folds it and the static_assert passes.
Line 3Uncommenting the bad line gives a compile-time diagnostic, not an exception, because constant evaluation cannot throw.
Line 4With b coming from a runtime variable the same body is ordinary code, so the exception is thrown and caught normally.
Important notes
Compile these with -std=c++20: loops and mutable locals in constexpr need C++14, single-argument static_assert needs C++17, and is_constant_evaluated and consteval need C++20.
A constexpr function is implicitly inline, so it belongs in a header, but the full definition must be visible at every point where you constant-evaluate it; a declaration alone makes the call non-constant.
Common mistakes
Writing int n = pow_int(2, 5); and assuming the loop disappeared: nothing forces folding in that context, and an unoptimised build really calls the function.
Using a function parameter as the argument, as in void f(int k) { constexpr int v = pow_int(2, k); }: a parameter is never a constant expression, so this fails to compile even though pow_int is constexpr.
Dropping a std::cout or a call to a non-constexpr helper into the body: the function still compiles, and the error surfaces later at the static_assert as "call to non-constexpr function", far from the edit.
Try it yourself
Change, predict, then run
Write constexpr std::array<int, 10> triangles() that fills entry i with i * (i + 1) / 2, initialise it as constexpr auto t = triangles();, add static_assert(t[9] == 45); and print the values. Then remove constexpr from the variable and confirm the program still prints the same numbers while the static_assert no longer compiles.
Open the C++ workspaceCheck your understanding
A constexpr function f(int) is called three ways: constexpr int a = f(2);, int b = f(2);, and int c = f(x); where x is an ordinary runtime int. What is true?
- All three are evaluated during compilation, because f is declared constexpr.
- The call with the runtime int fails to compile, because a constexpr function requires constant arguments.
- Only the constexpr variable's initialisation is guaranteed to be evaluated during compilation; the plain int may or may not be folded; the runtime-argument call cannot be.
- None are evaluated during compilation unless f is also declared inline.
Show answer
Only a context that requires a constant expression forces the compiler to run the body, and the constexpr variable is the sole such context here; int b = f(2); is an ordinary initialisation the compiler may fold as an optimisation but is not obliged to, and f(x) has no constant value to compute. Option 1 is tempting because constexpr looks like a compile-time-only marker, but a constexpr function is also a normal function and machine code is emitted for exactly this call.