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 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 that 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 differ, 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 this is okay in many situations, there is one where this pattern doesn't work. You may already know this. Marking a variable as constexpr also implicitly makes this variable 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 suggest that 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. You cannot achieve this with another new compile-time keyword in C++20, constinit, as constinit works only with static initialized data.

Book an in-house C++ training class

Do you like this content?

I'm available for in-house C++ training classes worldwide, on-site or remote. Here is a sample list of my classes:
  • From C to C++
  • Programming with C++11 to C++17
  • Programming with C++20
All classes can be customized to your team's needs. Training services

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 get in touch with me on X or via email.

Andreas