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