C++20: A neat trick with consteval
source link: https://andreasfertig.blog/2021/07/cpp20-a-neat-trick-with-consteval/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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:
constexpr int Calc(int x)
{ A
return 4 * x;
}
int main()
{
auto res = Calc(2); B
}
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
:
constexpr int Calc(int x)
{ A
return 4 * x;
}
int main()
{
constexpr auto res = Calc(2); B
}
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:
consteval int Calc(int x)
{ A consteval now
return 4 * x;
}
int main()
{
auto res = Calc(2); B Compile-time due to consteval
++res; C Modify res at run-time
}
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):
consteval auto as_constant(auto value)
{
return value;
}
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:
constexpr int Calc(int x)
{ A constexpr again
return 4 * x;
}
int main()
{
B Forcing compile-time with as_constant
auto res = as_constant(Calc(2));
++res; C Modify res at run-time
res = Calc(res); D Run-time use of Calc
}
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
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK