Hello, and welcome to C++ Weekly. I'm your host, Jason Turner. In this episode I need to clear up
some misconceptions and misunderstandings about constexpr and, I think, give you a best practice
for how to move forward with your constexpr usage in the future.
This episode of C++ Weekly is sponsored by me. Be sure to check out the video description for
important links about upcoming events, sponsoring the channel with Patreon, buying copies of my Best
Practices and Puzzler books and how to contact me if you are interested in on-site training or
remote code reviews. Now just a quick important note before I continue - this episode is being
recorded and aired at this particular moment because I need this information for background
in the next episode that I will be recording shortly after this one. So you really should
subscribe and make sure that you see the next episode to come up because I think it's a pretty
good one - I hope it'll be a good one. Okay, so I have here "constexpr - probably doesn't do
what you think it does," and I know when I do a title like this it tends to be a little bit,
uh, click bait. Makes people respond, "Oh, of course, I knew exactly what constexpr did
this entire time", so I don't know what I'm gonna actually title this episode just yet,
but hopefully it'll be something creative. The misconceptions around constexpr go something
like this. Let's go ahead and create a function, and it's just going to be called 'get_value'
where, you know, it's going to be pretty trivial. We're just going to
return two times the value passed into us, and then down here in 'main' I am going to
use this value. So if I have this... and now I'm getting a error. Let's see. We want to do 6
x 2, that's going to be 12. We're going to return the value here. Now the question that we have is,
"Is this value calculated at compile time?" Now if you look here at the Compiler Explorer output
we're probably not going to be surprised to see that it returns 12. That's all this
program is doing. The optimizer has done this work for us. So let's just go ahead
and say, "Is this value calculated at compile time?" Um, you know, for this version,
maybe - (it's) up to the optimizer. But there is a second question that we have to ask and that is,
"Is the value "usable in constant expressions"?" This might sound a little
meaningless without a little bit more context. So we're asking, "Is it usable
in a constant expression?" and the simplest way to check this is to do a 'static_assert'.
We're going to static_assert that 'value' equals 12, and we're going to get a compile time error
because this is a "non-constant condition for static assertion." This is not usable
in a constant expression. Now if we make it const, and you've seen me cover this in previous videos,
then it falls under other rules related to things that are called "core constant expressions." This
is constant initialized. It is available in a constexpr context. Let's comment this back out
again, and now we still have this question of "Is this value calculated at compile time?" and the
answer is still maybe. It's up to the optimizer whether or not this is calculated at compile time.
Now if I do a static assertion here, then yes, it is calculated at compile time. Because I required
the value in this constant expression context, now the compiler was required to give me this
value at compile time. So let's comment this back out again and let's make this constexpr,
and I'm still asking, "Is this value calculated compile time?" Interestingly,
the answer is still maybe because we haven't done anything to require
the compiler to calculate this at compile time. Now I will say in my experience GCC
is very aggressive about calculating values at compile time. (It's outside the scope of this
video to show where and how.) LLVM - somewhere in between, and then MSVC is somewhat lazy about
this. It's only going to calculate something at compile time if it must, which now starts to bring
us to the point of this video. So constexpr here - we have really no idea if this thing is going
to be calculated at compile time. The only way to force compile time calculation is to do this
static assertion down here. So let's go ahead and let's make a new function.
So let's look at this, you know, theoretically relatively expensive thing to calculate here;
and we are going to go ahead and assign this to a local variable.
It would be helpful if the entire thing was not just initialized to zero. Okay, so the compiler
has optimized all this away. It's returning 2,637 for me, which sounds about right as 879 x 3,
but it was not required to. So let's go ahead and take this back to -O0,
and we are in GCC. So we can see that the compiler has for us calculated (this for us). Let's look at
this here - this "sub 4000 from rsp." Now what the compiler has done here is it has created a
space for 4,000 bytes (that is 4k) because we've got 1,000 integers, they are four bytes wide each,
and it has created a space for 4,000 bytes for us; and then it is going to copy this data in.
See this "repeat move sq"? It is copying the data from this compile time array into the local stack,
and that brings us to the point of this video. Maybe this will be the name of the video - it is
"constexpr values are stack values." Maybe that's what we need. Well I've experimented a bit here,
and I have been unable to get an actual stack overflow to occur,
which is a little disappointing but it's also not really relevant here. I think that you've
gotten the point that constexpr values are stack values. They're not some magic thing that exists,
you know, in static initialization in the compiler. They're not required to be calculated
at compile time. In this particular case any compiler here could go back to doing this thing at
runtime instead of compile time. It is not required to. But I really want to drive home
this point about this being a stack variable. Now in real world code it's entirely possible
for this to actually take a fairly significant amount of time to execute because it now has to
copy this large blob of data onto the stack and possibly do some calculations there. So let's
go ahead and illustrate this. I'm just going to create a pointer to an integer, initialize it
properly with a null pointer, and then I am going to put an explicit scope around this.
I am inside of here going to say "'p' equals some value, let's say 985."
Like that. Okay. So I have set this up now. I have 'p' initially initialized to 'nullptr',
then I have it set to the address at the object at location 985 (that's the 986th
location inside of my array), and then I'm going to uncomment this line. The question is for you,
"What value is going to be returned from the program here?" So we know that it should be
985 times 3, not, you know, there's no surprise here. Is it going to be the constant 985 times 3,
is it going to be some runtime calculated value or is it going to be something else entirely?
So go ahead and make up your mind. Now I'm going to uncomment this.
Now see, Clang here has me returning 2955, which is probably correct and this is at -O3. Let's
go ahead and move to a slightly newer version of Clang and we're just going to kind of click around
here and try different compilers for a minute. I'm not getting any warnings here, I'm just getting a
value returned. Ah, interesting. I'm getting this fun error from GCC now saying this value "is used
uninitialized." This is a fun error. Whenever you get this "this value is used uninitialized" you
know that you have, well, undefined behavior somewhere in your code. Either you didn't
initialize the value or the compiler thinks that you didn't initialize the value. Well we know that
we did initialize the value and you're telling me that it's uninitialized. That means that there's a
different bug somewhere in the code. So let's go ahead and take this back down to like -O0 again,
and did anything change? We're getting 139 returned. All right. Zero. The warning goes away.
My goodness. This is more difficult to track down than it should be.
Let's go ahead and turn on address sanitizer. Oh. Here we go - "stack-use-after-scope." So hopefully
I'm starting to make my point here. This 'values' - this is a stack value. This thing exists here.
So on this line, on line 24, I am getting a pointer to a temporary. When this gets popped
from the stack then I have a pointer to an invalid object. The optimizers for both Clang and GCC were
hiding that from me. GCC's optimizer gave me a nonsensical warning. Clangs optimizer told me that
the whole world was fine, which is clearly wrong. That's even outside of the purview of the 'as-if
rule' because there was clear undefined behavior in the code. Let's see. I'm going to turn on Clang
with address sanitizer. I get the exact same error and if I turn the optimization level back up,
then we're going to just get 2955 returned. So there's a couple of best practices here.
The first is - you must run all tests with your address sanitizer enabled.
You need to run both release and debug builds for your test with address sanitizer enabled
if you want to catch these errors, but at some point I really need to get back to
the point of this video. Constexpr is almost always not what you mean, or put another way,
you very rarely want constexpr variables. There is a very simple solution to this problem and
it eliminates the entire question of where the values live, and what their lifetimes are, and
when and how they are initialized; and it is quite simply to make this a 'static constexpr' value.
You almost always mean 'static constexpr'. Static constexpr should be your go to,
not constexpr, so the title of this video might end up being "Stop Using Constexpr and Use This
Instead" - "this" being static constexpr. So if we go ahead and just take this back
down an optimization level or two, something that's going to actually give us this failure
that we were talking about - see even -O1 makes the entire program go away. This is a trivial
example and that's part of the problem. This kind of thing can actually show up as real problems
in their code. Okay, so now we're not going to get the address sanitizer error because this is
correct. "Constexpr values are stack values unless they unless they're statics." We rarely want
constexpr variables, we almost always mean static constexpr. This is going to force initialization
at compile time, and it's going to force a topic. Here under [basic.start.static] we have "constant
initialization is performed if a variable or temporary object with static or thread storage
duration is constant-initialized." So we are forcing constant initialization of this object,
and that's exactly what we want from constexpr most of the time. So it says here this is
"zero initialization and constant initialization are called static initialization," and "static
initialization strongly happens before dynamic initialization." So we know that these things
have to happen before the program starts. That's what you want - you want that value calculated.
You don't care where it lives, and you want it done before the program starts.
We should, if we are careful here, be able to actually see where it is not copying the data
out. Let's see. We move 985 into 'esi', and then we call "operator bracket" on this array
of that particular value, and we are passing in a 'this' pointer of 'main:: values'. So we can see
here that we've got 'main::values' - we don't copy anything onto the local stack. It's all guaranteed
to happen at compile time, and depending on the situation, the compiler might actually be
able to eliminate this entire data structure if it can have full visibility into your program.
So that's the point that I wanted to get to. We wanted to force initialization at compile time.
Note that the compiler didn't say anything else about constexpr variables. Those are not required
to be constant initialized, only static constexpr variables are; and as you saw constexpr is not a
storage duration type. Now if you had actually done my Object Lifetime Puzzlers book, which
I've mentioned a few times on this channel and I have links to in the video description down here,
you would have known that constexpr is not one of the types of storage duration. We've got
'thread', 'static', 'automatic' and 'dynamic'. A constexpr variable that's not static is an
'automatic' variable. Now I don't mean 'auto' type deduced, I mean automatic storage duration.
Well hopefully this video was educational. It was far longer than I intended it to be,
but there's some best practices in here and there's some ways for you to hopefully think
more about constexpr in the future; and I don't think I missed anything in this one.
So be sure to subscribe. Be sure to check out that next episode coming up.