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 that is 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, which 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 be different, 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 in a lot of situations, this is okay, there is one where this pattern doesn't work. You may already know this. Marking a variable as constexpr
also makes this variable implicitly 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 would probably make 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. This is something you cannot achieve 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 reach out to me on Twitter or via email.
Andreas