constexpr Functions: Optimization vs Guarantee

constexpr Functions: Optimization vs Guarantee

By Andreas Fertig

Overload, 33(186):12-13, April 2025


Constexpr has been around for a while now, but many don’t fully understand its subtleties. Andreas Fertig explores its use and when a constexpr expression might not be evaluated at compile time.

The feature of constant evaluation is nothing new in 2023. You have constexpr available since C++11. Yet, in many of my classes, I see that people still struggle with constexpr functions. Let me shed some light on them.

What you get is not what you see

One thing, which is a feature, is that constexpr functions can be evaluated at compile-time, but they can run at run-time as well. That evaluation at compile-time requires all values known at compile-time is reasonable. But I often see that the assumption is once all values for a constexpr function are known at compile-time, the function will be evaluated at compile-time.

I can say that I find this assumption reasonable, and discovering the truth isn’t easy. Let’s consider an example (Listing 1).

constexpr auto Fun(int v)
{
  return 42 / v; 
}

int main()
{
  const auto f = Fun(6); 
  return f;              
}
Listing 1

The constexpr function Fun divides 42 by a value provided by the parameter v . In , I call Fun with the value 6 and assign the result to the variable f.

Last, in , I return the value of f to prevent the compiler optimizes this program away. If you use Compiler Explorer to look at the resulting assembly, GCC with -O1 brings this down to:

  main:
          mov     eax, 7
          ret

As you can see, the compiler has evaluated the result of 42 / 6, which, of course, is 7. Aside from the final number, there is also no trace at all of the function Fun.

Now, this is what, in my experience, makes people believe that Fun was evaluated at compile-time thanks to constexpr. Yet this view is incorrect. You are looking at compiler optimization, something different from constexpr functions.

Let’s remove the constexpr from the function first (Listing 2).

auto Fun(int v)
{
  return 42 / v; 
}

int main()
{
  const auto f = Fun(6); 
  return f;              
}
Listing 2

The resulting assembly, again GCC and -O1 is the following:

  Fun(int):
          mov     eax, 42
          mov     edx, 0
          idiv    edi
          ret
  main:
          mov     eax, 7
          ret

Okay, that looks more like proof that constexpr helped before. You now can see the function Fun, but the result is still known in main. Why is that?

The reason is that constexpr implies inline! Try for yourself, make Fun inline, and you will see exactly the same assembly output as when the function was constexpr.

Because of the implicit inline, the compiler understands that Fun never escapes the current translation unit. By knowing that there is no reason to keep the definition around. Then, Fun itself is reasonably simple to the compiler, and the parameter is known at compile-time. An invitation for the optimizer, which it happily accepts.

You can alter the code even more, and the optimizer will still be able to produce the same result. Have a look at the changes I made to the original code in Listing 3.

inline auto Fun(int v) 
{
  return 42 / v;
}

int main()
{
  int  val{6};       
  auto f = Fun(val); 
  return f;
}
Listing 3

Fun is now inline as shows. The input to Fun is now a non-const variable var , and the result of the call to Fun in is stored in a non-const variable. All just run-time code. Except that the compiler can still see that the input to Fun is always 6. With this knowledge the compiler gets its friend the optimizer onboard and the result is the same as with the initial code that looked way more constant then this version.

What you see here is still an optimization. Yes, if you are interested in a small binary footprint, you will be happy. But, constexpr can give you more! You can get guarantees from constexpr. Let’s explore that.

Ways to enforce constant evaluation

The current code does not force the compiler to evaluate Fun at compile-time in a manner that could cause compile-time evaluation to fail. The evaluation could silently fail for integral data types declared const, which isn’t allowed with constexpr. Essentially, you must force the compiler into a compile context for the evaluation. You have roughly four options for doing so:

  • assign the result of Fun to a constexpr variable;
  • use Fun as a non-type template argument;
  • use Fun as the size of an array;
  • use Fun within another constexpr function that is forced into constant evaluation by one of the three options before.

In Listing 4, you find the four cases in code.

constexpr auto Other(int v)
{
  return Fun(v);
}

int main()
{
  constexpr auto f{Fun(6)};
  int            data[Fun(6)]{}; // Please prefer 
                       // the std::array solution
  std::array<int, Fun(6)> data2{};
  constexpr auto          ff{Other(6)};
}
Listing 4

Enforcing constant evaluation

So far, I have done neither of the four variants, time to change this. Let me make the variable f constexpr (Listing 5).

constexpr auto Fun(int v)
{
  return 42 / v; 
}

int main()
{
  constexpr auto f = Fun(6); 

  return f; 
}
Listing 5

Once you look at the resulting assembly, you see ... no change compared to the initial example. Remember that I started by stating that distinguishing optimization from the guarantee is difficult?

My example now comes with the guarantee that Fun is evaluated at compile-time. However, since there is no difference between the former version in the resulting assembly, what is my point?

Well, time to start talking about the guarantee.

What if, and please don’t be shocked, I replace 6 with 0 in my call to Fun? Urg, yes, that will result in a division by zero. Who, aside from Chuck Norris, can divide by zero? At least, I can’t, and neither can any of the compilers I use.

But the initial example, despite the fact that Fun is constexpr, compiles just fine. Well, this little warning about the division by zero aside. Ah, yes, and the result is, well, potentially the result to expect if one of us could divide by zero.

The guarantee

Make the variable f in constexpr, or choose another way to force the compiler into constant evaluation. The result? If you make the change, your compile will fail, and the compiler tells you the obvious: a division by zero does not produce a constant value. This is what constexpr functions bring you: an evaluation free of undefined behavior!

Putting constexpr on a function only gives you a small part of constexpr. Only by using a constexpr function in a context requiring constant evaluation will you get the full benefits out of it, no undefined behavior.

I hope this article helps you better understand what constexpr can offer and how to distinguish the guarantee from a compiler’s optimization.

References

The code listings are available on godbolt:

This article was published on Andreas Fertig’s blog on 6 June 2023, and is available at: https://andreasfertig.com/blog/2023/06/constexpr-functions-optimization-vs-guarantee/

Andreas Fertig is a trainer and lecturer on C++11 to C++20, who presents at international conferences. Involved in the C++ standardization committee, he has published articles (for example, in iX) and several textbooks, most recently Programming with C++20. His tool – C++ Insights (https://cppinsights.io) – enables people to look behind the scenes of C++, and better understand constructs.






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.