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 aconstexpr
variable; - use
Fun
as a non-type template argument; - use
Fun
as the size of an array; - use
Fun
within anotherconstexpr
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:
- Listing 1: https://godbolt.org/z/chG8oe3TG
- Listing 2: https://godbolt.org/z/85W5Mdv4T
- Listing 4 (with the extra needed for it to compile): https://godbolt.org/z/ddrGrYr3M
- Listing 5: https://godbolt.org/z/4Y5nraeYG
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/
https://cppinsights.io) – enables people to look behind the scenes of C++, and better understand constructs.
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 (