Logo

Blog


C++20: A neat trick with consteval

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:

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
6
7
8
9
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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):

1
2
3
4
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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