C++20: A neat trick with consteval
Among the various improvements of C++20 are changes to constexpr, namely a new keyword, consteval. In this post, I like to dig into consteval a bit and see what we can do with this new facility.
What consteval does
As the name of the keyword tries to imply, it forces a constant evaluation. In the standard, a function marked as consteval is called an immediate function. The keyword can be applied only to functions. Immediate here means that the function is evaluated at the front end, yielding only a value that the back end uses. Such a function never goes into your binary. A consteval-function must be evaluated at compile-time or compilation fails. With that, a consteval-function is a stronger version of constexpr-functions. We have now the choice:
- Compile-time only (
consteval) - Compile- or -run-time (
constexpr) - Run-time (no attribution required)
The figure below visualizes the three different variants:

The behavior of consteval is handy in a situation where you like to ensure that a certain function is always evaluated at compile-time.
We already have constexpr
Now, let's circle back and see what we can do with constexpr and where things get complicated.
A typical pattern I see in my training classes is the following:
1 2 3 4 5 6 7 8 9 | |
In A, we have a constexpr-function, so far so good. Then in B, this function gets called, and the result is stored in res. The natural expectation is that Calc is evaluated at compile-time. All criteria are met:
- The function is marked as
constexpr; - All input values are constants.
However, Calc is evaluated at run-time. Depending on your optimizer and optimization level, things may differ, but Calc is called at run-time from a standards point. What is missing is making the variable res itself constexpr:
1 2 3 4 5 6 7 8 9 | |
In this version, we achieved what we wanted. Calc is called at compile-time because the variable itself is marked as constexpr (B). While this is okay in many situations, there is one where this pattern doesn't work. You may already know this. Marking a variable as constexpr also implicitly makes this variable const. If you struggle here, use C++ Insights to show you what constexpr brings piggyback.
Now, assume that we like to have that call to Calc happen at compile-time, but res should be writable at run-time. This is where we can use consteval, to force evaluation at compile-time, regardless of the constexpr'ness of the variable:
1 2 3 4 5 6 7 8 9 10 11 | |
Your new friend: as_constant
All right, so far, so good. In the version above, Calc is now a compile-time-only function. Now, what if we like to have both? Calc should be usable at compile- and run-time. But at the same time, we like res to be writable at run-time. Let me introduce you to as_constant, a handy new helper (you have to copy or write yourself):
1 2 3 4 | |
Yes, as_constant appears to be a very silly function. The function simply returns its input without any modification. I suggest that you remove such a silly function in a code review. But thanks to the consteval modifier, as_constant serves a greater purpose:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
In A, Calc is constexpr again. We use as_constant in B to force compile-time evaluation of Calc. As before, we can modify res in C, but we can now also use Calc at run-time as D shows. You cannot achieve this with another new compile-time keyword in C++20, constinit, as constinit works only with static initialized data.
Since as_constant is evaluated purely at compile-time, the by-value semantic is okay. No need to care about moving things.
One thing is left to mention: with the approach shown with as_constant, the destructor of the type used in the function must be constexpr.
I hope you learned something today. If you have other techniques or feedback, please get in touch with me on X or via email.
Andreas
