Skip to content

Compile-Time Programming with constexpr

Beginning of the lesson

Compile-Time Programming with constexpr

Move computation out of run time and into the compiler - the modern successor to template metaprogramming

Big idea: The 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.
By the end of this topic you will be able to:
  • distinguish a constexpr function (may run at compile time) from a consteval function (must);
  • know when a constexpr variable forces compile-time evaluation, and how that differs from plain const;
  • use if constexpr to discard branches at compile time and retire much of your SFINAE and tag-dispatch code;
  • explain why constexpr has 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 constexpr variable, or a static_assert condition;
  • a non-type template argument, e.g. std::array<int, factorial(5)>;
  • an array bound, an enumerator value, a case label.
A 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.

Continue this lesson on YourAIZone

The full lesson has the complete explanation, worked examples, common mistakes, tests with instant checking and an AI tutor. Start right away as a guest, or create an account to keep your progress.

In the full lesson

  • The complete explanation
  • Worked examples
  • Common mistakes and how to avoid them
  • Test questions: 15
  • AI tutor that answers questions about the topic
  • Progress tracking for students, parents and teachers