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.
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 (
- Compile- or -run-time (
- 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
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
- All input values are constants.
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
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:
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
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
as_constant serves a greater purpose:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
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 works only with static initialized data.
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
I hope you learned something today. If you have other techniques or feedback, please reach out to me on Twitter or via email.