Advanced constexpr Function Techniques
Use loops, local variables, and other features inside constexpr functions.
What Is a Required Constant Expression? (Part 2)
Almost everything in this lesson falls out of one distinction, and the distinction belongs to the context a call sits in, not to the function being called.
Some places in C++ cannot proceed without a value in hand before the program is built. The initializer of a constexpr variable is the obvious one; the length of a fixed-size array and a template argument are two more you will meet later. These are contexts that require a constant expression.
Everywhere else merely permits one. Initializing an ordinary int, handing an argument to std::cout, adding two numbers inside a loop: a compile-time answer would be welcome, but a value computed while the program runs is just as acceptable.
Marking a function constexpr grants it entry to the first group. It says nothing about where any particular call gets evaluated. The context decides that.
Compile-time evaluation is guaranteed only where the language requires a constant expression. Everywhere else the compiler chooses, and choosing run time is a legitimate choice.
Where a Constexpr Call Can Land
Rather than work up to the rule, here it is as a table. Every constexpr call you write falls into one of these five rows.
| Where the call appears | Compile-time evaluation |
|---|---|
| In a context requiring a constant expression | Always, guaranteed by the standard |
| Inside a function that is itself being evaluated at compile time | Always, for the same reason |
| Nowhere in particular, all arguments are constant expressions | Probably, nothing stands in the way |
| Nowhere in particular, an argument is a variable whose value the compiler happens to know | Possibly, purely as an optimization |
| Nowhere in particular, an argument is not known until the program runs | Never, the information does not exist yet |
A hive inspection tally makes the middle rows concrete. cappedCells() converts a count of frames into an approximate number of capped cells, and the four calls sit in four different contexts.
#include <iostream>
constexpr int cappedCells(int frames)
{
return frames * 3500;
}
int main()
{
constexpr int wintering{ cappedCells(8) }; // case 1
std::cout << "case 1: " << wintering << '\n';
std::cout << "case 2: " << cappedCells(8) << '\n'; // case 2
int frames{ 8 };
std::cout << "case 3: " << cappedCells(frames) << '\n'; // case 3
std::cout << "How many frames did you count? ";
std::cin >> frames;
std::cout << "case 4: " << cappedCells(frames) << '\n'; // case 4
return 0;
}
Input:
12
Output:
case 1: 28000
case 2: 28000
case 3: 28000
How many frames did you count? case 4: 42000
All four calls produce a correct answer, which is exactly why the difference is easy to miss. What separates them is when the multiplication happened.
- Case 1 initializes a
constexprvariable, so a constant expression is required. The multiplication happens during compilation, and no compiler is permitted to defer it. - Case 2 feeds
std::cout, which cannot run before the program does. Nothing requires a constant here. The argument is still a literal, so the compiler is able to fold the call, and in practice it usually will, but it is not obliged to. - Case 3 passes
frames, an ordinaryint. Not being a constant expression, it drags the call out of the guaranteed rows. The compiler can see thatframesstill holds8at that point, so under the as-if rule it may fold the call anyway. Run time is the more likely outcome. - Case 4 passes a value that came from
std::cin. No amount of analysis recovers a number the user has not typed yet, so this call always runs at run time.
Notice that "constant expression" and "value the compiler knows" are not the same thing. Case 3 has the second without the first, and that is precisely the gap where the guarantee disappears.
What Actually Decides the Middle Rows
Three separate mechanisms are at work in the rows that say "probably" and "possibly", and none of them is under your control.
Optimization level. Both GCC and Clang decline to evaluate a constexpr call at compile time in a non-required context unless they have been asked to optimize, for example with -O2. A debug build normally has optimizations off, so the same source file can evaluate case 2 at compile time in release and at run time in debug.
The as-if rule. A compiler may transform a program however it likes as long as the observable behavior is unchanged. Folding a calculation into a constant is such a transformation, which is why case 3 is even a possibility. The same freedom applies to functions that are not marked constexpr at all, so a plain function can also end up evaluated during compilation.
Inlining and elimination. A call may be inlined into its caller, or removed entirely if its result is never used. Either changes when, or whether, the body runs.
Never write code whose correctness depends on a call landing in the "probably" or "possibly" rows. If a computation has to happen at compile time, put it somewhere a constant expression is required.
A Function That Compiles Until You Need It
The compiler does not have to check that a constexpr function is capable of compile-time evaluation until something actually asks it to evaluate one at compile time. That leaves room for a function which looks fine, runs fine, and then collapses the first time it matters.
The version below reads its tally from a helper that is not marked constexpr. It is broken, and deliberately so.
#include <iostream>
int tallyFromLedger(int frames) // not constexpr
{
return frames * 3500;
}
constexpr int cappedCells(int frames)
{
if (frames < 0)
return 0;
return tallyFromLedger(frames); // calls a non-constexpr function
}
int main()
{
int runtimeTotal{ cappedCells(8) }; // compiles and runs
std::cout << runtimeTotal << '\n';
constexpr int frozenTotal{ cappedCells(8) }; // will not compile
std::cout << frozenTotal << '\n';
return 0;
}
Delete the frozenTotal line and the program builds and prints 28000. Put it back and the compiler finally has to try:
s.cpp: In function 'int main()':
s.cpp:21:43: in 'constexpr' expansion of 'cappedCells(8)'
21 | constexpr int frozenTotal{ cappedCells(8) }; // will not compile
| ~~~~~~~~~~~^~~
s.cpp:13:27: error: call to non-'constexpr' function 'int tallyFromLedger(int)'
13 | return tallyFromLedger(frames); // calls a non-constexpr function
| ~~~~~~~~~~~~~~~^~~~~~~~
s.cpp:3:5: note: 'int tallyFromLedger(int)' declared here
3 | int tallyFromLedger(int frames) // not constexpr
| ^~~~~~~~~~~~~~~
The diagnostic points at line 13, inside a definition the compiler read and accepted without complaint. Nothing was wrong with cappedCells() on its own; the fault only exists once compile-time evaluation is demanded of it.
Marking the helper constexpr repairs both calls, and initializing a constexpr variable from the result is how you prove it:
#include <iostream>
constexpr int tallyFromLedger(int frames) // now constexpr
{
return frames * 3500;
}
constexpr int cappedCells(int frames)
{
if (frames < 0)
return 0;
return tallyFromLedger(frames);
}
int main()
{
constexpr int frozenTotal{ cappedCells(8) }; // the compile-time test
int runtimeTotal{ cappedCells(8) };
std::cout << frozenTotal << " capped at compile time\n";
std::cout << runtimeTotal << " capped, timing up to the compiler\n";
return 0;
}
Output:
28000 capped at compile time
28000 capped, timing up to the compiler
Every constexpr function should be usable at compile time, since a caller may put it anywhere a constant expression is required. Exercise each one from a
constexpr variable initializer at least once. A function that only ever gets called at run time has never really been tested.
Before C++23, a constexpr function for which no argument values permit compile-time evaluation made the program ill-formed, no diagnostic required. The
if (frames < 0) return 0; line above exists to supply one such path and keep the broken example strictly legal. C++23 removed the requirement through P2448R1. Because no diagnostic was ever required, compilers rarely enforced it in the first place.
Parameters Carry Values, Not Constness
A natural next thought is that if the arguments were constant expressions, the parameters inside the function must be too. They are not, and you cannot declare them that way either.
The reason is the run-time half of the deal. A constexpr function is allowed to be called with ordinary variables, as case 3 and case 4 above did. If a parameter were constexpr, the function could only ever accept constant expressions, and half of what makes constexpr useful would vanish. So parameters stay ordinary values, which in turn means they cannot appear anywhere a constant expression is required inside the function body.
Both lines below break that rule. The code is broken on purpose.
#include <iostream>
consteval int perFrame(int cells) // cells is not constexpr
{
return cells / 2;
}
constexpr int wintering(int frames) // frames is not constexpr either
{
constexpr int fixedFrames{ frames }; // will not compile
return perFrame(frames) + fixedFrames; // will not compile
}
int main()
{
constexpr int autumnCount{ 8 };
std::cout << wintering(autumnCount) << '\n';
return 0;
}
s.cpp: In function 'constexpr int wintering(int)':
s.cpp:10:32: error: 'frames' is not a constant expression
10 | constexpr int fixedFrames{ frames }; // will not compile
| ^~~~~~
s.cpp:12:20: error: call to consteval function 'perFrame(frames)' is not a constant expression
12 | return perFrame(frames) + fixedFrames; // will not compile
| ~~~~~~~~^~~~~~~~
s.cpp:12:21: error: 'frames' is not a constant expression
12 | return perFrame(frames) + fixedFrames; // will not compile
| ^~~~~~
Note that autumnCount in main() genuinely is a constant expression. That constness does not survive the call. Once the value arrives as frames, it is just a value, so it cannot initialize a constexpr variable and it cannot be handed to a consteval function, which insists on constant expression arguments. The same applies to consteval functions themselves: their parameters are not constexpr either, even though such functions only ever run at compile time.
You may declare a constexpr function's parameters const, which makes them run-time constants and stops the body from reassigning them. That is a different property from being a constant expression.
When you genuinely need a parameter that is a constant expression, the tool is a non-type template parameter, covered in a later chapter.
Why the Compiler Needs the Whole Definition
To evaluate a call during compilation, the compiler has to execute the function body itself. A declaration tells it the name, the parameter types and the return type, which is enough to emit a call instruction but nowhere near enough to run the code. So a declaration alone fails:
#include <iostream>
constexpr int broodArea(int frames); // declaration only
int main()
{
constexpr int total{ broodArea(6) }; // will not compile
std::cout << total << '\n';
return 0;
}
constexpr int broodArea(int frames)
{
return frames * 640;
}
s.cpp: In function 'int main()':
s.cpp:7:35: error: 'constexpr int broodArea(int)' used before its definition
7 | constexpr int total{ broodArea(6) }; // will not compile
| ~~~~~~~~~^~~
A forward declaration is never enough for compile-time evaluation of a
constexpr or consteval function. The full definition has to be visible by the time the evaluation happens.
Now follow that requirement across a multi-file project. If two source files both evaluate broodArea() at compile time, both need the definition, and two definitions of the same function in one program would normally violate the one-definition rule. C++ resolves this by making every constexpr function implicitly inline, which exempts it from that rule. The practical consequence is that constexpr functions belong in headers.
// apiary.h
#pragma once
constexpr int cappedCells(int frames)
{
return frames * 3500;
}
// main.cpp
#include "apiary.h"
#include <iostream>
int main()
{
constexpr int wintering{ cappedCells(8) };
std::cout << wintering << " cells of stores\n";
return 0;
}
Output:
28000 cells of stores
Define a constexpr or consteval function in a header when more than one source file uses it, so each translation unit gets the full definition. When only one source file uses it, define it in that file above its first call.
Calls that will only ever be evaluated at run time are the exception: for those, a declaration is enough, because the linker supplies the body later. That does let you call a constexpr function defined in another translation unit, but only from contexts that never demand a constant.
CWG2166 states the requirement more precisely than "defined before the call". The definition must appear before the outermost evaluation that eventually reaches the call. That wording is what makes mutually recursive constexpr functions possible, and it makes the example below legal.
#include <iostream>
constexpr int broodArea(int frames); // declaration only, definition further down
constexpr int hiveArea(int frames)
{
return broodArea(frames) + 240; // broodArea() has no definition yet
}
constexpr int broodArea(int frames) // defined before any call is evaluated
{
return frames * 640;
}
int main()
{
constexpr int total{ hiveArea(6) }; // the outermost evaluation starts here
std::cout << total << " square centimeters\n";
return 0;
}
Output:
4080 square centimeters
hiveArea() calls broodArea() before that function has a body. That is fine, because nothing evaluates hiveArea() until main() initializes total, and by then both definitions have been seen.
Summary
The deciding factor is context, not the keyword. constexpr on a function grants permission to appear in a constant expression. Where the call is written determines whether it must be evaluated during compilation.
Guaranteed: a call in a context requiring a constant expression, such as a constexpr variable initializer, or a call made from a function already being evaluated at compile time.
Likely but not promised: a call with all-constant arguments in a context that does not require a constant. The compiler chooses, and at -O0 GCC and Clang generally choose run time.
Possible only as an optimization: a call whose argument is a variable the compiler happens to know the value of, folded under the as-if rule. Plain non-constexpr functions can be folded the same way.
Impossible: a call whose argument is not known until the program runs.
Diagnosis is deferred. A constexpr function is not checked for compile-time viability until something evaluates it at compile time, so one can pass every run-time test and still fail to compile. Initialize a constexpr variable from each one to force the check.
Parameters are plain values. They are not implicitly constexpr, cannot be declared constexpr, and cannot be used where the body requires a constant expression, including as arguments to consteval functions. const is allowed and means something weaker.
Definitions must be visible. A forward declaration cannot be evaluated. Constexpr functions are implicitly inline and therefore exempt from the one-definition rule, which is what lets you place them in headers and include them everywhere they are needed.
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.
Advanced constexpr Function Techniques - Quiz
Test your understanding of the lesson.
Practice Exercises
Constexpr Function Evaluation Contexts
Create a program demonstrating when constexpr functions evaluate at compile time versus runtime. Test constexpr functions in various contexts and verify their evaluation timing.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!