Immediate Functions with consteval
Force compile-time evaluation with consteval and detect compile-time context.
What Are Consteval and Forced Compile-Time Evaluation? (Part 3)
constexpr on a function is permission, not instruction. It says the function may run at compile time when the context demands a constant, and leaves the compiler free to run it at run time otherwise. This lesson is about the two things that permission does not give you: a way to insist, and a way to find out which happened.
The Gap That constexpr Leaves
There is no way to tell the compiler "prefer compile time whenever you can". What you can do is force a single call, by using its result somewhere a constant expression is mandatory. Initializing a constexpr variable is the usual trick:
constexpr int peak{ brighter(120, 340) }; // must be computed at compile time
It works, but the price is a variable that exists only to force the evaluation. Do it often and the code fills with names that mean nothing to a reader.
consteval: Must, Not May
C++20 adds consteval, which marks a function as an immediate function: every call must be evaluated at compile time, and any call that cannot be is a compile error.
#include <iostream>
consteval int brighter(int left, int right)
{
return (left > right ? left : right);
}
int main()
{
constexpr int peak{ brighter(120, 340) };
std::cout << peak << " lumens\n";
std::cout << brighter(120, 340) << " lumens\n";
return 0;
}
Output:
340 lumens
340 lumens
Both calls are computed during compilation, including the second, which is not initializing anything constant. That is the difference from constexpr: the guarantee comes from the function, not the context.
Feed it a value the compiler does not know and it refuses:
#include <iostream>
consteval int brighter(int left, int right)
{
return (left > right ? left : right);
}
int main()
{
int measured{ 120 };
std::cout << brighter(measured, 340) << " lumens\n";
return 0;
}
This does not compile, which is the entire point:
s.cpp: In function 'int main()':
s.cpp:12:26: error: call to consteval function 'brighter(measured, 340)' is not a constant expression
12 | std::cout << brighter(measured, 340) << " lumens\n";
| ~~~~~~~~^~~~~~~~~~~~~~~
s.cpp:12:27: error: the value of 'measured' is not usable in a constant expression
12 | std::cout << brighter(measured, 340) << " lumens\n";
| ^~~~~~~~
Reach for
consteval when a function has to run at compile time, typically because it does something only possible there. Everywhere else constexpr is the better default, since it also works at run time.
One detail catches people out: the parameters of a consteval function are not implicitly constexpr, even though the function only ever runs at compile time. That was a deliberate consistency decision, not an oversight.
Can You Tell Which Way a Call Went?
Not reliably. C++ offers no mechanism that answers "did this particular call evaluate at compile time".
std::is_constant_evaluated(), from <type_traits>, is the closest thing and it answers a subtly different question. It reports whether the code is running in a constant-evaluated context, meaning a context where the language requires a constant expression, such as initializing a constexpr variable.
The gap matters because a compiler is also free to evaluate a constexpr function at compile time in a context that did not require it. In that case the function really did run at compile time, and std::is_constant_evaluated() still returns false.
Read
std::is_constant_evaluated() as "the compiler is obliged to evaluate this now", not "this is evaluating at compile time".
Two reasons it is defined that way:
- The standard draws no formal line between compile time and run time. Wording a feature in those terms would have meant a far larger change than adding one function.
- Optimizations are not allowed to change observable behavior. If the answer flipped whenever the optimizer happened to fold a call, then a program branching on it could produce different results at different optimization levels.
C++23's if consteval is a cleaner spelling of if (std::is_constant_evaluated()) and fixes some rough edges, but it reports the same thing.
Forcing a constexpr Call Without a Throwaway Variable
consteval functions guarantee compile-time evaluation but cannot run at run time, which makes them less flexible than constexpr. What we actually want is to keep a constexpr function and force compile-time evaluation at a chosen call site.
The trick is that arguments to a consteval function are themselves required to be constant expressions. Pass a constexpr call as an argument to an immediate function and it has no choice but to evaluate during compilation:
#include <iostream>
#include <type_traits>
consteval auto forceCompileTime(auto computed)
{
return computed;
}
constexpr int reading(int left, int right)
{
if (std::is_constant_evaluated())
return (left > right ? left : right);
return (left < right ? left : right);
}
int main()
{
int measured{ 120 };
std::cout << "runtime call: " << reading(measured, 340) << '\n';
std::cout << "plain call: " << reading(120, 340) << '\n';
std::cout << "forced call: " << forceCompileTime(reading(120, 340)) << '\n';
return 0;
}
Output:
runtime call: 120
plain call: 120
forced call: 340
reading deliberately returns different answers in the two contexts so you can see which path ran. The first call cannot be a constant expression, so it takes the run-time branch. The second is a plain call whose result feeds std::cout, so nothing requires a constant and it also takes the run-time branch. Only the third, wrapped in the immediate function, is forced into a constant-evaluated context.
The wrapper returns by value. At run time that could be wasteful for an expensive type, but here the whole call is replaced by the computed value during compilation, so nothing is copied at all.
forceCompileTime uses an auto parameter, which makes it an abbreviated function template, and an auto return type. Both are covered later; neither is necessary to use the technique. Some GCC 14 builds returned the wrong answer for this pattern with optimizations enabled. The compiler behind this site's code runner, GCC 16 at -O2, produces the correct 340 shown above, but it is worth verifying on your own toolchain before relying on it.
Summary
constexpr is permission: it allows compile-time evaluation where a constant is required, and permits run-time evaluation everywhere else. There is no way to ask for "compile time whenever possible".
Forcing one call: use the result where a constant expression is mandatory, such as initializing a constexpr variable. It works but litters the code with variables that exist only for that purpose.
consteval: declares an immediate function that must evaluate at compile time, turning any call that cannot into a compile error. Its parameters are not implicitly constexpr.
std::is_constant_evaluated(): reports whether the code is in a context that requires a constant expression, not whether the call is evaluating at compile time. A compiler may fold a call opportunistically and still report false.
Why it is defined that way: the standard has no formal compile-time versus run-time distinction, and optimizations must not alter observable behavior.
if consteval: the C++23 spelling, same semantics, nicer syntax.
Forcing without a variable: pass the constexpr call as an argument to a consteval wrapper. Arguments to immediate functions must be constant expressions, so the inner call is evaluated at compile time and the wrapper hands back the result.
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.
Immediate Functions with consteval - Quiz
Test your understanding of the lesson.
Practice Exercises
Consteval and Forcing Compile-Time Evaluation
Learn the difference between constexpr and consteval functions. Create immediate functions that must always evaluate at compile time and understand when to use each.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!