Compile-Time Programming with constexpr
Move computation out of run time and into the compiler - the modern successor to template metaprogramming
constexpr family tells the compiler that a function or variable is eligible to be evaluated during compilation. When the inputs are themselves compile-time constants, the result is computed by the compiler and baked into the binary as a literal - no work is left for the CPU at run time. Where template metaprogramming (TMP) encoded computation as recursive template instantiations that read like puzzles, constexpr lets you write ordinary functions - with loops, local variables and if - that simply happen to also run at compile time.
- distinguish a
constexprfunction (may run at compile time) from aconstevalfunction (must); - know when a
constexprvariable forces compile-time evaluation, and how that differs from plainconst; - use
if constexprto discard branches at compile time and retire much of your SFINAE and tag-dispatch code; - explain why
constexprhas largely superseded TMP for computing values, and name the cases where templates and TMP are still the right tool.
This builds on Template Metaprogramming, SFINAE and Constraints and Concepts (C++20) from earlier in this course. We assume you can already read a recursive template and a type trait.
1. constexpr functions - permission, not a promise
A constexpr function is a normal function with one extra property: it is permitted to run during compilation. Whether it actually does depends entirely on the call site. Called in a constant-evaluated context with constant arguments, the compiler evaluates it and stores the answer as a literal. Called with an ordinary run-time value, it behaves like any other function and runs on the CPU. One definition, two possible execution times.
constexpr long factorial(int n) {
long result = 1; // C++14: locals, loops and
for (int i = 2; i <= n; ++i) // mutation are all allowed
result *= i;
return result;
}
constexpr long f5 = factorial(5); // evaluated by the compiler -> 120
int r = read_int(); // some run-time value
long fr = factorial(r); // same function, runs at run time
Notice how readable that is compared with the recursive-template factorial you met in Template Metaprogramming. It is ordinary imperative code. Since C++14 a constexpr function may contain loops, multiple statements, local variables and mutation of those locals; the C++11 restriction to a single return is long gone.
What counts as a constant-evaluated context? The places where the language requires a constant expression:
- the initializer of a
constexprvariable, or astatic_assertcondition; - a non-type template argument, e.g.
std::array<int, factorial(5)>; - an array bound, an enumerator value, a
caselabel.
constexpr function that can never produce a constant for any arguments is ill-formed (no diagnostic required). But a valid constexpr function called with run-time arguments is perfectly fine - it just runs at run time.