What Is Compile-Time Function Evaluation For?

Every lesson in this chapter answered variations of one question: when does this function call actually run? A call that the compiler resolves during the build leaves nothing behind in the executable except its answer. The program starts with the work already done, and any mistake in that work is a compile error rather than a bug someone reports later.

That payoff is the reason the chapter exists. The rest of it is about the machinery that decides whether you get the payoff, and the machinery is smaller than it looks: two keywords, one rule about context, and a handful of consequences that follow from the compiler having to run your code while it is still reading it.

This recap is organised around that machinery rather than around the lesson order, so you can use it as a reference when you are unsure why a particular call did or did not fold away.

Three Kinds of Function, Side by Side

Almost every question in this chapter is answered by a single row of this table.

Plain function constexpr function consteval function
Callable where a constant expression is required No Yes Yes
Callable at run time Yes Yes No
Evaluated during compilation Only as an optimization Guaranteed only where a constant is required Always, by definition
Implicitly inline No Yes Yes
Needs its full definition visible to fold n/a Yes Yes
Its parameters are constant expressions inside the body No No No
Its arguments must be constant expressions No No Yes

Read the second and third rows together and the whole constexpr versus consteval distinction falls out. constexpr is permission: the function is allowed into a constant expression, and it keeps working normally everywhere else. consteval is an obligation: the function is an immediate function, so every call is evaluated during compilation and a call that cannot be is rejected.

Key Concept
Marking a function constexpr says nothing about when any particular call runs. It widens where the function may be called. The call site decides the rest.

Where a Call Is Guaranteed to Run at Compile Time

A context requiring a constant expression is a place in the language that cannot proceed without a value already in hand. The initializer of a constexpr variable is the one you meet first. A static_assert condition, the length of a std::array, a non-type template argument, an enumerator value and a case label are the others you will run into.

Put a constexpr call in one of those places and compile-time evaluation is guaranteed. Put it anywhere else and the compiler chooses.

#include <iostream>

constexpr int trimmedWidth(int gutter)
{
    return 148 - gutter;
}

int main()
{
    constexpr int lockedMargin{ trimmedWidth(12) }; // guaranteed at compile time
    int workingMargin{ trimmedWidth(12) };          // the compiler decides

    std::cout << "locked:  " << lockedMargin << '\n';
    std::cout << "working: " << workingMargin << '\n';
    std::cout << "printed: " << trimmedWidth(12) << '\n'; // the compiler decides

    return 0;
}

Output:

locked:  136
working: 136
printed: 136

Three identical answers, and only the first was promised to you. The second and third have literal arguments and an optimizing build will almost certainly fold them, but nothing in the language obliges it to, and a debug build with optimizations off generally will not. If a computation has to happen during the build, write it somewhere a constant is required. If you only wish it would, you have written a wish.

What You May Write Inside One

The common misconception is that a compile-time function is some restricted dialect of C++. It is not. When the compiler evaluates one, it effectively runs the body, so ordinary imperative code is fine: non-const local variables, loops, branches, and reassignment of the parameters themselves.

The point that catches people is what happens when such a local or parameter is passed on to another constexpr call.

#include <iostream>

constexpr int leavesPerSheet(int folds)
{
    int leaves{ 1 };

    for (int fold{ 0 }; fold < folds; ++fold)
        leaves += leaves; // every fold doubles the leaf count

    return leaves;
}

constexpr int pagesInSignature(int folds)
{
    int leaves{ leavesPerSheet(folds) }; // an ordinary local, not constexpr
    return leaves + leaves;              // each leaf is printed on both sides
}

int main()
{
    constexpr int signaturePages{ pagesInSignature(3) };
    static_assert(signaturePages == 16);

    std::cout << "a three-fold signature carries " << signaturePages << " pages\n";

    return 0;
}

Output:

a three-fold signature carries 16 pages

Neither folds nor leaves is a constant expression. They are plain values, and the language will not let you declare a parameter constexpr or use one where a constant is required inside the body. Yet pagesInSignature(3) initializes a constexpr variable, so the whole thing was computed during the build, and the static_assert proves it.

The resolution is that these are two different properties. Being a constant expression is a compile-time category. Being a value the compiler knows is a fact about a particular evaluation. Once the compiler has committed to evaluating pagesInSignature(3) at compile time, it necessarily knows that folds is 3 and leaves is 8, so it can carry on into leavesPerSheet with those values. What it cannot do is let you treat the parameter as a constant expression in code that also has to compile for the run-time case.

Warning
A constexpr function is only checked for compile-time viability when something actually evaluates it at compile time. One that calls a non-constexpr helper can pass every run-time test and still fail to build the day a caller puts it in a constant expression. Exercise each one from a constexpr variable initializer or a static_assert at least once.

Why the Definition Has to Be in Scope

To fold a call, the compiler has to execute the body. A forward declaration gives it the name and the types, which is enough to emit a call instruction and nothing like enough to run the code. So a declaration alone is never sufficient for compile-time evaluation.

Follow that requirement across a project and the second consequence appears. If three source files each evaluate the same constexpr function at compile time, all three need the definition, and three definitions of one function would normally break the one-definition rule. C++ resolves the conflict by making constexpr and consteval functions implicitly inline, which exempts them from that rule. This is why compile-time functions belong in headers rather than in a single source file.

Best Practice
Put a constexpr or consteval function in a header when more than one source file uses it. When only one file uses it, define it in that file, above its first call.

The Mistakes This Chapter Exists to Prevent

The two keywords fail in different ways, and seeing both failures next to each other is the fastest way to remember which is which. The program below is broken on purpose.

#include <iostream>

constexpr int trimmedWidth(int gutter)
{
    return 148 - gutter;
}

consteval int platesPerForme(int inks)
{
    return inks + 1; // one plate per ink, plus one for the varnish
}

int main()
{
    int measuredGutter{ 12 };
    int inkCount{ 4 };

    std::cout << trimmedWidth(measuredGutter) << '\n'; // fine, evaluates at run time
    std::cout << platesPerForme(inkCount) << '\n';     // will not compile

    return 0;
}
s.cpp: In function 'int main()':
s.cpp:19:32: error: call to consteval function 'platesPerForme(inkCount)' is not a constant expression
   19 |     std::cout << platesPerForme(inkCount) << '\n';     // will not compile
      |                  ~~~~~~~~~~~~~~^~~~~~~~~~
s.cpp:19:33: error: the value of 'inkCount' is not usable in a constant expression
   19 |     std::cout << platesPerForme(inkCount) << '\n';     // will not compile
      |                                 ^~~~~~~~
s.cpp:16:9: note: 'int inkCount' is not const
   16 |     int inkCount{ 4 };
      |         ^~~~~~~~

Both calls pass an ordinary int variable. The constexpr call shrugs and evaluates at run time, which is exactly what the permission model allows. The consteval call has nowhere to go, because an immediate function has no run-time form, and its arguments therefore have to be constant expressions themselves. Making one word of the program constexpr fixes it:

#include <iostream>

constexpr int trimmedWidth(int gutter)
{
    return 148 - gutter;
}

consteval int platesPerForme(int inks)
{
    return inks + 1; // one plate per ink, plus one for the varnish
}

int main()
{
    int measuredGutter{ 12 };
    constexpr int inkCount{ 4 }; // now a constant expression

    std::cout << trimmedWidth(measuredGutter) << " mm of type\n";
    std::cout << platesPerForme(inkCount) << " plates on the forme\n";

    return 0;
}

Output:

136 mm of type
5 plates on the forme

The rest of the chapter's failure modes are variations on the same misunderstanding:

Symptom What went wrong Fix
"is not a constant expression" at a consteval call The argument was a run-time value Make the argument a constant expression, or use constexpr instead
"used before its definition" Only a declaration was visible Move the definition above the call, or into a header
"call to non-constexpr function" inside a constexpr body A helper was never marked Mark the helper, or branch on std::is_constant_evaluated()
Declaring a parameter constexpr Parameters carry values, not constness Use a non-type template parameter when you truly need a constant
Assuming a call folded because it could have The context did not require a constant Initialize a constexpr variable, or wrap the call in a consteval function
Rule
Never write code whose correctness depends on a call being folded in a context that does not require a constant. Optimization level, inlining and the as-if rule all move that line, and none of them are under your control.

Key Terminology

  • Constant expression: an expression the compiler can evaluate while building the program
  • Context requiring a constant expression: a place, such as a constexpr initializer or a static_assert condition, that cannot accept a value computed later
  • Constexpr function: a function permitted to appear in a constant expression, and free to run at either compile time or run time
  • Consteval function: a function that must be evaluated during compilation, also called an immediate function
  • Implicitly inline: exempt from the one-definition rule, which is what lets a compile-time function be defined in a header
  • Constant-evaluated context: what std::is_constant_evaluated() reports on, meaning a context where the language demands a constant

Looking Forward

Compile-time evaluation stops being a curiosity as soon as you meet features that require it. Non-type template parameters, the length of a std::array, and later the compile-time selection performed by if constexpr all consume constant expressions, and each of them becomes far more usable once you can call a function to produce the value. Each C++ release has also widened what a constexpr body may contain, so the honest summary is that the restrictions you are learning today are the tightest they will ever be.

Summary

The benefit: work resolved during compilation costs nothing at run time, and errors in that work become compile errors.

constexpr is permission: the function may be called in a constant expression, and may also be called at run time. Where the call is written decides which happens.

consteval is an obligation: an immediate function is always evaluated during compilation, and its arguments must be constant expressions.

Guaranteed folding happens only in contexts that require a constant expression, such as a constexpr variable initializer, a static_assert condition, an array length or a template argument. Everywhere else the compiler chooses, and at low optimization levels it usually chooses run time.

Bodies are ordinary code: non-const locals, loops, branches, and modified parameters are all fine. During a compile-time evaluation the compiler knows every value, so locals and parameters may be passed on to other compile-time calls.

Parameters are values, not constants: they cannot be declared constexpr and cannot be used where the body requires a constant expression, in constexpr and consteval functions alike.

Definitions must be visible: a forward declaration cannot be evaluated. Both kinds of function are implicitly inline and therefore exempt from the one-definition rule, which is what puts them in headers.

Diagnosis is deferred: a compile-time function is only checked for compile-time viability when something demands it, so run-time success proves nothing. Force the check with a constexpr variable or a static_assert.