Skip to content
C Codeloom
C++

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.

·4 min read · By Codeloom
Intermediate 10 min read

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)
constexpr vs consteval vs constinit

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 if f is not constexpr; the value is set at runtime and never changes. constexpr int x = f(); requires f itself to be constexpr and the call to be a constant expression.
  • Forgetting that templates trigger evaluation: using a constexpr function in a template parameter slot forces it to be evaluated. If anything inside isn’t constexpr, 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 new and delete in 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.