Compile-Time Function Evaluation Summary
Review and test your understanding of all compile-time function evaluation concepts covered in this chapter.
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.
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.
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.
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 |
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
constexprinitializer or astatic_assertcondition, 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.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Compile-Time Function Evaluation Summary - Quiz
Test your understanding of the lesson.
Practice Exercises
Compile-Time Calculator
Create a compile-time calculator using constexpr functions. This exercise demonstrates how to leverage the compiler to perform calculations at compile time, improving runtime performance and enabling compile-time validation.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!