C++ constexpr and Compile-Time Computing
How constexpr, consteval, and constinit let you move computation from runtime to compile time, with practical patterns and the rules that govern them.
What you'll learn
- ✓What constexpr means for functions and variables
- ✓How consteval and constinit narrow the meaning
- ✓When the compiler must vs may evaluate at compile time
- ✓Practical examples: lookup tables, compile-time parsing
- ✓Limits of constexpr and how they relaxed across standards
Prerequisites
- •Familiarity with basic C++ functions and templates
What and Why
C++ has steadily expanded what you can compute at compile time. constexpr lets functions and variables participate in compile-time evaluation. consteval (C++20) requires it. constinit (C++20) requires constant initialization without forcing constness afterward. Together, they let you move work from runtime to translation, catch errors earlier, and produce smaller binaries with no dynamic startup cost.
Mental Model
Think of constexpr as a contract that says “I can be evaluated at compile time if the inputs are known then.” The compiler will run it during translation when used in a constant context (array bounds, template arguments, if constexpr), and at runtime otherwise.
constexpr : may run at compile time or runtime
consteval : must run at compile time (immediate function)
constinit : initialization must be constant; value can mutate later
const : value cannot mutate after init (says nothing about when) Hands-on Example
A compile-time factorial and a small lookup table baked into the binary.
#include <array>
#include <iostream>
constexpr long long factorial(int n) {
long long acc = 1;
for (int i = 2; i <= n; ++i) acc *= i;
return acc;
}
template <std::size_t N>
constexpr std::array<long long, N> make_factorials() {
std::array<long long, N> table{};
for (std::size_t i = 0; i < N; ++i)
table[i] = factorial(static_cast<int>(i));
return table;
}
constexpr auto kFactorials = make_factorials<13>();
static_assert(kFactorials[5] == 120);
consteval int square(int x) { return x * x; } // must be const-evaluated
int main() {
std::cout << kFactorials[10] << '\n';
constexpr int s = square(7); // OK: arg is a constant
// int n = std::rand(); square(n); // ERROR: not constant
std::cout << s << '\n';
}
The static_assert proves the table is computed before the program runs. The consteval form makes square refuse to be used with a runtime argument, which is sometimes exactly what you want (think strongly typed unit literals).
Common Pitfalls
- Confusing const with constexpr:
const int x = f();is fine even iffis not constexpr; the value is set at runtime and never changes.constexpr int x = f();requiresfitself to be constexpr and the call to be a constant expression. - Forgetting that templates trigger evaluation: using a
constexprfunction in a template parameter slot forces it to be evaluated. If anything inside isn’tconstexpr, you get a hard error. - Floating-point and undefined behavior: any UB in a constexpr context becomes a compile error rather than silently miscompiling. That is a feature, but it surprises people moving runtime code into constexpr.
- Dynamic allocation rules: C++20 allows
newanddeletein constant expressions, but only when the allocation is freed before evaluation ends. Leaks become compile errors. - Lambdas: lambdas are implicitly constexpr in C++17+ when their body permits it. Marking them explicitly is sometimes clearer.
Practical Tips
Reach for constexpr to encode invariants. A compile-time check is worth ten unit tests, because the program literally cannot compile if the invariant breaks.
For lookup tables, prefer compile-time generation to hand-tuned arrays. It is easier to audit and easier to change.
Use if constexpr to remove dead branches in templates instead of SFINAE. The intent reads cleanly:
template <typename T>
void print(const T& v) {
if constexpr (std::is_integral_v<T>) std::cout << "int: " << v;
else std::cout << "other: " << v;
}
Pair constexpr with static_assert at module boundaries. It documents preconditions and refuses to compile if anyone violates them.
When in doubt about whether something is a constant expression, try assigning it to a constexpr variable. The compiler will tell you exactly what is wrong.
Wrap-up
Compile-time computing in modern C++ is no longer a niche metaprogramming trick. With constexpr, consteval, and constinit, you can shift checks, tables, and parsing from runtime to translation with normal-looking code. The result is faster startup, smaller binaries, and bugs caught before the program even runs. Start by marking pure functions constexpr and see how often the compiler accepts the upgrade for free.
Related articles
- C++ C++ CMake Tutorial: Build Your First Project
Learn modern CMake for C++ — targets, properties, and dependencies — and configure a small project that compiles cleanly across platforms.
- C++ C++ Coroutines: An Introduction
Understand C++20 coroutines — co_await, co_yield, co_return — and how they enable async and generator-style code without blocking threads.
- C++ C++ Design Patterns Overview
Tour the most useful design patterns in modern C++ — singleton, factory, observer, strategy, RAII — and learn when each one earns its keep.
- C++ C++ Exceptions vs Error Codes
Compare exceptions and error codes in C++ for handling failures — performance, ergonomics, and when each style fits real codebases.