8

C++20: A neat trick with consteval

 2 years ago
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.
neoserver,ios ssh client
This post is a short version of Chapter 12 Doing (more) things at compile-time from my latest book Programming with C++20. The book contains a more detailed explanation and more information about this topic.

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:

Compile and run-time split of the keywords

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK