Cookies   I display ads to cover the expenses. See the privacy policy for more information. You can keep or reject the ads.

Video thumbnail
- This is Surprises in Object Lifetime.
And there's still people straggling in,
I know that'll happen for the next few minutes.
Now, um,
Andrei Alexandrescu is in this room
and he was just telling me that his goal
is to be loud enough to get our room
to complain about the noise.
And then I was told that we should make it our goal
to get Andrei's room to complain about the noise.
But, if you ever been to my talks,
I'm not great at giving jokes.
When I do, they tend to fall flat.
But maybe we can come up with something
to cheer at some point.
Maybe if we hear them getting loud, we can figure it out.
All right.
So, my name is Jason Turner.
This is my intro slide, if you'd been to my talks,
you've probably seen something like this before.
Couple of things I'll just call out.
I am host of C++ weekly, which is my YouTube channel,
and co-host of CppCast, which is a podcast.
I will mention,
C++ weekly on Friday more.
I'll ask that real quick here.
Who watches my YouTube channel?
Oh, it's pretty good, cool.
Okay, um.
Yes, I am independent
and available for training or contracting
if you're interested in having me come
as your trainer to your company
that, you can usually be worked out.
Yes, move to the front.
There are people in the back,
if you know who I am and have seen my talks before,
they are highly interactive.
Gasper's getting in the front, good job.
We will be doing things in here hopefully that are fun.
And also this is approximately what training looks like,
so you have an idea of what to expect
if you do hire me to come to your company.
So, I have a couple of upcoming things I want to mention.
This is good as people are straggling in still.
CppCon post conference training,
it is not too late to sign up for that if you're interested.
It's two days, Saturday and Sunday, right here.
Well not right here, it's
upstairs.
I will be giving a one day training at C++ on C,
if you're not familiar with it.
That's conference that's gonna be in Southern England
for the first time in February in 2019.
Now, this is a hypothetical.
We are working on the concept for this of a
three day training thing that I'm considering doing with
Matt Godbolt and Charley Bay in the Denver area
in the summer.
That should be interesting because we have very different
perspectives on things.
So, you can hear us argue about things if you'd like to.
Okay, so, about Surprises In Object Lifetime,
I have taught this one day course called,
Understanding Object Lifetime about 12 times now.
Well-defined object lifetime,
and our construction, destruction cycle in C++
is one of the key features of C++.
We want to be able to well reason about
the lifetime of things.
And we're one of just the few languages where we can
reason about the lifetime of objects.
Sometimes there are surprises however.
Sometimes they are for the better, sometimes for the worse.
So, let's start with what is an object?
This is a key definition
if we're gonna talk about object lifetime.
And I am cheating the lights at the moment,
so I'm off-center.
They're not blinding me as much and I can see you some more.
Okay, how is this thing, this struct that contains an int,
different from an int?
Yell stuff out.
This is how it works in my talks.
It has a label?
Somebody said.
So there's some sort of type.
- [Man] It has a different name.
- It has a different name.
That's, that's, that's okay, yes.
Gasper says it has a different name.
So, if we were to look at them from this perspective,
they have the same size,
using uniform initialization syntax.
They can be initialized the same way.
We can refer to them both as a reference to an integer,
I say don't do this in real code on line six.
Please don't do this in real code,
but it is technically legal.
We can reinterpret_cast a struct is the first element of it.
We can assign them the same way
and access them the same way.
They look pretty similar.
So, if we look at some of our type traits,
they are both constructable, trivially destructible,
trivially copyable, and more to the point,
they are both objects.
So, what is an object?
The standard says,
an object type is a possibly const volatile
qualified type that is not a function,
not a reference, and not void.
Pretty much everything is an object.
Okay,
so, Object Lifetime.
Lifetime begins when storage with the proper alignment
and size for a type is obtained,
and if the object has non-vacuous initialization,
its initialization is complete.
At some point,
I have this slide in a couple of different talks.
At some point I should remove the bit about union,
because we don't care about that in this exact moment.
Although adding union to our surprises
could have been an interesting thing to do,
I did not do it in this talk.
So, if it has non-vacuous initialization,
once the initialization is complete.
It's lifetime ends
when if it is a class with a non-trivial destructor,
the destructor call starts,
or the storage in which the object as occupied is released
or reused, okay?
Any questions?
We're gonna do examples and interaction
and stuff in a minute, yeah?
- [Man] Do you have a definition
for non-vacuous constructor?
- Let's say non-vacuous initialization,
let's say non-trivial initialization.
Yes? - [Man] Why do they not
use that more familiar defined term?
- Why did they not use the more familiar term?
I honestly do not know.
And if there's someone who does know
why vacuous is used here?
I don't know.
But that's a great question.
Okay,
So, I have my simple utility here,
that I've got the struct S,
it has something that prints
in each of the special member functions,
and then I've got main,
I am creating an object of type S on line 12,
I am creating a scope,
I'm creating an another object called s2 on line 15.
Alright, what does this code print
if I were to execute it?
Or probably no one's gonna yell it out.
I will say, what is printed on line 12?
(audience mumbling)
Line three, S.
So, it's S with the parens, okay.
What is printed on line 14?
- [Man] Nothing. - Nothing, nothing, yes.
Not everything prints on main.
On line 15, what is printed?
S, const S ref,
so the copy constructor is called on line 15.
Now,
I have this kinda annotation I personally use,
you'll catch on in a second here.
What's printed on line 16?
(audience mumbling)
The destructor.
Yes, the destructor of s2 is called on line 16,
then was how I just called on line 17.
(audience mumbling)
The destructor of S.
So, we end up with this.
Now,
just for the record,
if we needed to at any moment in our talk
we can flip over the Compiler Explorer
and actually test these things.
So, if we have any questions at all,
stop me, we can prove though that this what is prints.
S, then the copy-constructor, destructor, destructor.
Good?
Okay.
Okay, now what is printed?
What's printed on line 12?
Constructor of S.
Okay,
we already said line 14 doesn't print anything,
that's no change there.
What's printed in line 15?
(audience mumbling)
Nothing, nothing is printed.
This is perfectly safe.
I haven't done any kind of implementation defined,
undefined thing, or nothing,
nothing is printed.
What's that?
(audience mumbling)
It's a reference.
It's a reference, there's no copy to be made.
Then, line 16,
nothing.
Line 17,
(audience mumbling)
destructor, okay.
So, that's what it looks like.
So, remember that reference types are not object types.
They don't, we don't talk about,
they don't affect our lifetime,
mostly.
We will show something where they do.
Okay, alright,
now we're gonna get into this.
So, what is printed in this code?
I have this function called get_data,
it is returning this constant ref
and then I'm printing it on line 9.
What'd you say, 42?
(audience mumbling)
That's as good a guess as any.
Most likely five, okay.
(audience mumbling)
Alright, so,
we are returning a reference to local.
It's garbage, this is unknown, this is undefined behavior.
What warning do we get from the compiler?
(audience mumbling)
Returning a reference to local,
sorry, it's, for some reason it's easier
for me to focus over here.
If I am ignoring you, someone start doing jumping jacks
or something, burpies, I don't know,
something that I'll notice,
and plus it's good because you're sitting all week,
basically anyhow.
Alright, so, yes,
we might get a reference to stack memory return,
reference to local return, something like that.
Are we surprised yet?
The title of this talk is Surprises in Object Lifetime,
hopefully, we get to some surprises in a minute.
Okay, now, what is printed?
Now I'm returning a reference_wrapper not a reference.
It is still unknown, yes.
We are still returning a reference to a local object.
So, this reference_wrapper is implicitly
taking a reference to i, the local value,
and returning it.
The question is,
now what warning do we get from the compiler?
(audience mumbling)
Probably nothing,
unless it's magic.
(audience mumbling)
What's that, I'm sorry.
(audience mumbling)
Oh, oh, you're saying with like
the stuff that Herb is working on right now,
yes, no.
I'm just talking about gcc and clang at the moment.
Okay,
um,
so,
I'm compiling this list here, my default's clang,
I've got w_all, w everything,
w_shadow, and Pitantic.
No warnings are printed,
we can see that in the lower right hand corner.
Gcc makes this fun,
because if I change it in gcc.
Sorry, wrong gcc.
We get this warning.
Warning "i" is used uninitialized in this function.
(audience mumbling) Is i initialized
in this function?
I'm pretty sure I is initialized.
This has become like a rule for me now,
if I see a warning that makes literally no sense at all,
I've decided I'm probably invoking
some type of undefined behavior.
Yeah?
(audience mumbling)
Um,
the only thing, so the question was,
you said, it's talking not about it's initialization
but it's actually use inside of main.
Yes, but the only place that we as humans see an i
is in that function.
And it has been initialized there.
That is why I take issue with this.
What's that?
Would it go with, I used no end line.
I don't know.
I can turn off all optimizations.
No longer get the warning, yeah.
So, the compiler doesn't have enough information
to see that here, yes.
Okay,
alright.
So, simple standard library wrappers
around references confuse our analysis.
That's our first surprise!
Opps, sorry.
Let's talk about strings.
What is printed from main?
(audience mumbling)
Hello World, does anyone question
that Hello World is printed?
(audience mumbling)
What's that?
Global data, okay.
Hello World is printed, why is it allowed?
The standards specifically says evaluating a string literal
results in string literal object
with static storage duration,
it's effectively a global, as you said.
Initialized from the characters specified.
Static objects are valid for the entirety of the program.
This is fine, this is totally allowed.
Okay,
we're now returning a string view.
What is printed?
Still Hello World.
Okay,
because string view is basically a pair of pointers,
to the beginning and end of the string.
And we're not surprised right?
Alright, now what is printed for main?
(audience mumbling)
42, unknown.
What's that?
It might not get to the point of printing.
But, most likely, the answer is Hello World
says someone in, who shall remain nameless,
in the front row.
(audience mumbling)
So,
with our magic of Compiler Explorer here,
we can execute this.
Um,
Yeah, that's, that's printing nothing,
it says that returned zero,
it says it successfully executed,
but it didn't print any data.
And we can
(audience mumbling)
that is gcc, turn off the optimizer,
it doesn't, it doesn't change anything really.
Um,
Yeah, something like that,
that's an interesting unicode character
that it decided to pick.
Okay,
so this is unknown, why?
I think most of you already got this.
Since the string view is pointing to
the beginning and end of the string,
the string object that actually stored this data
was popped from the stack and our local string
has gone out of scope.
We already saw it in the Compiler Explorer,
what warnings did we get from this code?
Not a single thing.
Okay.
What does this code print?
I'm getting confident Hello World answers.
(audience mumbling)
I'm getting a few undefined.
Why, who wants to describe why undefined?
Someone, somebody, you wanna go with why,
okay, why?
(audience mumbling)
Yeah, you raised you hand, right?
(audience mumbling)
And it's a, yes.
(audience mumbling)
Right
so it's not very much of a change from stood string
because this is a local array called S
that is initialized with the global data
that character literal Hello World
and then we are returning a string view into that.
So, the local array decays to a pointer,
which initializes string view
and we get no warnings.
That's
fun.
Any questions?
So, surprise!
This is how I phrased it,
strings live longer than you think they will,
except when they don't.
I have actually, I will take in aside right here,
many of you raised you hands saying
you watch my YouTube channel.
I did say, explicitly in a recent episode
that this is the one feature I'd remove to C++.
Remove from C++.
Implicit conversion from array to a pointer has literally
no use in the language what so ever.
We could require a static_cast when it is necessary.
You might say backwards compatibility is seed,
but you can throw the static_cast in there
if you really need to.
So, that's the feature I'd remove.
Similar idea here,
we are now initializing a vector
and we want to ask what is printed here,
when we push back an object of type S,
with this constructor?
So, you're saying,
you're saying the S int constructor
and then the copy constructor
(audience mumbling)
and then destructor
(audience mumbling)
Okay, how many people say the copy constructor is called?
You have to have more confidence than this,
raise your hands higher.
Okay, who says the move constructor is called?
Okay.
Alright, so, we'll the votes win,
so move constructor is what it's called.
And that is because there is an overload for push_back
for the move constructor.
How many destructors do we see?
Okay, who says,
how many say one?
How many say two?
How many say three?
Okay,
I think two won also.
So, we'll go with two.
So, yes, we have the S int constructor,
the move constructor, destructor, and destructor.
So we remember that for non-trivial type,
the destructor of the moved-from object
must still be called, is the takeaway here.
Surprise!
Moved-from objects still have to be destroyed.
So, this is a surprise for maybe a quarter of you.
Yes,
and it's often non-trivial often inlining the destructor
causes code bloat because it has to decide,
for example,
with pointer, if there is some resources that need to,
excuse me,
for example, if stood string, it must decide
if there are some resources that need to be freed
or whatever.
Okay,
I have switched this is emplace_back.
(audience mumbling)
You say it's the same.
(audience mumbling)
Yes, it is exactly the same.
For exactly the same reasons you said.
Same thing as push_back,
we have been used emplace_back improperly here.
The point emplace_back is that we are trying
to call the constructor
of the object.
And so, I will build on that.
Now I'm using emplace_back this way.
So, now what do we see?
What's the first thing we got printed?
(audience mumbling)
S int and then what's the next thing printed?
(audience mumbling)
Constructor of S.
That's it.
This is the proper way to use emplace_back.
Any questions?
I just see a couple of faces that look confused,
but, okay.
If you're not going to ask questions,
it's your own fault.
So, this is the correct way to use emplace_back,
if we're gonna use it,
we're calling a constructor, is basically what we're doing.
Worth pointing out, this line here,
we can call it with zero periameters.
So, we're calling the default constructor.
So, even without a named object
we have to think about object lifetime.
Now, I do think that understanding object lifetime
is probably the most important part
of understanding how to write
good clean C++ that is efficient.
Alright,
what is printed
here?
Yes, what is printed?
What is printed on line 12?
S (, alright.
What's printed on line 13?
Hello World.
What's printed on line 14?
Okay, does it bother anyone that this is
a const reference on line 12?
Because it is a reference to a temporary.
Yeah.
(audience mumbling)
Oh wait, sorry.
Yes, Leslie?
No, if you don't have const it won't compile.
You can not take a non-const reference
to a temporary
unless you are compiling in a version of MSVC
that is in permissive mode.
(audience mumbling)
Specifically.
Yes.
So, S, Hello World,
~ S.
So, this is our surprise here.
Complex rules allow for lifetime extension of temporaries
that are assigned to references.
If you want to look this up in the standards,
it's in class.temporary.
It is a lot of confusing things to read.
Yes.
Oh, uh,
(audience mumbling)
we will.
Okay, so the question was
would there might actually be two different S objects,
in this case.
I will say, hypothetically, before C++17,
yes, but we'll dig into this in a little bit.
(audience mumbling)
Yes,
yes.
Actually, no wait, I have to get back to that.
Do our value references also expand a lifetime.
No, but universal ones do, says Gasper.
Or forwarding references if you will.
(laughs)
Okay, so for the sake of this slide,
since someone asked
and I had not actually spent time to think about it.
Let's run through this real quick.
The question was might there actually be
more than one S object created.
And I will say, by looking at it more closely,
definitely
no.
Because we are value initializing,
excuse me, we are initializing on line eight,
the return value S.
Directly initializing the return value.
We're not returning any local object at all.
And then we have a const reference
to the returned thing on line 12.
Under no argument,
can you say that there is more than one object S here,
I believe, except, Ben's gonna disagree with me.
(audience mumbling)
Oh, it's unrelated to that.
(audience mumbling)
Okay, we will not worry about that right now.
(audience laughing)
That's, I repeated the question for the YouTube video.
(audience laughing)
Okay, so, complex rules object lifetime.
Alright,
I am now initializing a const int reference on line two
with a
temporary, effectively, one, on line six,
and putting that in a const reference.
What values are returned from main here?
(audience mumbling)
Okay, lets do it this way,
who says one,
the two questions, the two options are going to be
one and undefined.
Who says one?
Okay, who says undefined?
Ou, it's like 50/50.
Okay,
since there was no consensus,
we have to go to the standard.
It says one is returned from main,
lifetime extension rules apply recursively
to member initializers.
I was researching the answer to the last slide
when I came upon this example in the standard,
and I thought,
I have to put this in my talk also.
(audience laughing)
The question was what and the answer is
go read class.temporary.
I think it took me four times of reading it.
You read the standard more often than I do,
you probably get it faster, but, yes.
Okay.
(audience mumbling)
I'm sorry, what that a question.
(audience mumbling)
Only for what kind of initializers?
Is it only for aggregate initializers?
Um, no, I don't think it has to be a member initializer,
I think I could have actually done a constructor
that took an initializer with a (mumbles)
reference into this.
I'm almost positive, yes.
I don't think it has to be, yeah, aggregate.
Alright,
how many dynamic allocations are in this code?
We got a vector of stood strings.
Don't worry about the version of the standard,
actually, any version of the standard that'll compile this.
Okay, we have a couple of guesses being yelled out.
How many people say one?
How many people say two?
Nobody says two,
how many people say three?
Okay, somebody who said one, make an argument.
Who said one, raise your hands again.
You.
(audience mumbling)
Small string optimization.
This is not something guaranteed by the standard,
but if you want to have a standard library
that's able to compete, you have small string optimization.
So, almost certainly one,
we will have our a and b,
we'll fall into small string optimization,
we're doing one allocation for the vector.
So, stood string is highly optimized,
do not underestimate it.
For real, if you saw the talk I gave at C++ Anal this year,
you will see that stood string messed
with a lot of my benchmarks.
Alright, now how many dyncamic allocations do we have?
(audience mumbling)
(audience laughing)
The comment was it's not long enough.
Let's say with,
how many people says there's one dynamic allocation still?
No one.
Two?
Three?
Four?
One person half heartedly raised his hand for four.
Okay, you want to change your vote?
Okay, wait, how many says there's five?
Exactly one, oh, oh
oh, okay,
alright, alright, I see who else is raising their hands
in the back here,
um,
okay.
You said five, as far as people who can actually interact
with me, why five?
(audience mumbling)
Yes.
Okay, so basically,
initializer_list can't be moved from
is what your effective answer was.
We have an initializer,
well, the answer is five.
Why, I'll back it down.
Because the compiler has done this for us,
it has created a underlying
array of string objects
so those must be initialized,
that is one and two,
and then we must allocate the space for the vector,
which is three,
and then we must copy those strings into the vector,
which is four and five.
Like that.
So, five allocations here.
Who was surprised by that one?
Everyone, except for
those three people.
Yes.
(audience mumbling)
why is it initializer_list,
oh, oh,
so, if you look at the
this is where I'm making the cameraman's job harder,
but, I'm sorry.
If you look at the,
the constructor that takes an initializer_list for vector,
it is an initializer_list of string,
therefore, the underlying array that is created
for the initializer_list object,
is itself, an array of string.
That is what the standard requires it to be.
That is why it is not an array of const (mumbles).
We will have, I think, more satisfying examples
for you in just a moment.
(audience mumbling)
Oh, an array of const strings,
and not a, because it's not movable.
And if you look at the
the accessors on initializer_list
it only has constant accessors anyhow.
So, it's irrelevant really,
We, a lot would have to change.
And if I did,
well, I did an hour and a half long rant
at C++ (mumbles) about that.
Called an initializer_list are broken.
So, I'm not going to go into it right now,
but, in much more.
Um,
(audience mumbling)
Can it be optimized?
Not in any way that I'm aware of.
No,
Hepalison with claying might be able to optimize it
but I've never seen claying alide two Heap operations.
I've only seen it alide one.
Yes?
(audience mumbling)
Let's keep going.
Oh, if you had to do it at runtime?
(audience mumbling)
Okay, if it were const data
and you can't make the, well,
do you know how many objects it is?
IF you always know the number of string,
you can do better.
(audience mumbling)
Yes?
You might get only four.
Uh,
of the two strings?
They both have, no,
you can't merge the two of them,
they have to be two diff--
(audience mumbling)
I don't believe any compiler would do that,
because each underlying string object
has to have a distinct pointer to it's data
that it can manage separately.
(audience mumbling)
And (mumbles) because,
oh, okay,
because of the as if rule
it's hypothetically possible
that some of the (mumbles) could be merged.
Okay,
(audience mumbling)
yeah.
(audience laughing)
He says I have seen things.
Okay,
initializer_list and vocations create hidden const arrays.
That's our surprise here.
Um,
how many dynamic allocations does this code have?
(audience mumbling)
Okay, how many, who says zero?
Okay, who says one?
Two?
Three?
Does anyone wanna go as high as five today?
(audience laughing)
Can I get six?
I have to go into like bidding mode here.
Okay.
It's zero, it is zero.
Because we're using C++17's (mumbles) type deduction,
it's going to deduce this as a const char *
of two elements,
no allocations.
So, yes,
if you know what the strings are
you can do perfect.
Okay.
Um, is this okay?
Yes, okay.
We know that our character literals are valid
for the life of the program.
Now how many dynamic allocations do we have?
(laughs)
One, who says one?
Two?
Uh,
pretty good number of people say two.
Three?
Four?
Hand full of people are saying four.
Okay, you're saying four, alright.
Yes, I overheard the answer already,
it is two because this is not an initializer_list.
What is the difference?
Well it is, okay,
it is an initializer space list
if you're looking this up in the standard.
It is not an object of type initializer_list.
You have to keep those separate
when we're talking about initializer_list.
I don't have the vocabulary to keep
them separate without saying all of those words.
(audience mumbling)
Oh,
I'm missing a pair of curly braces, okay.
Clang user.
(audience laughing)
You know there are situations with clang
where it says extra
braces
suggested
and you can put like 75
and it still giving you that warning.
Its a little,
(audience laughing)
um,
yes.
So, this is why.
Standard array has no constructor.
We are directly initializing the data of the array.
Now, interestingly,
you notice this
_M_elems,
I don't know if it's lib C++
or lib stood C++
that literally uses this name.
But, I did look it up.
I was reading through the source of array.
As one does.
And, um,
okay, so
we,
mere mortals are not allowed to create
an identifier with this name.
This is reserved for the standard,
an _ followed by a capital letter.
And therefore, we are also not allowed
to rely on the name of this thing.
It is undefined behavior if we access something
with this name as well.
So, they are, they,
the people who implement the standard library,
they're allowed to do this.
We're not allowed to do this, they're allowed to do this
and then they can punish us if we try to access this thing.
They're allowed to.
So,
this is, anyhow, we're directly initializing the array.
Yes,
(mumbles).
(audience mumbling)
Yes.
(audience mumbling)
Yes.
(audience mumbling)
Is there a reason why it wasn't characters
of array length?
Um,
if you have multiple,
I, I don't know the exact wording,
I don't know the exact place in the standard,
but I've
from my experience
if it were one of these,
it would be a character array, plausibly,
with two of them it has to deduce the common type,
which is gonna be char const star star.
That's, I'm almost positive that's what's going on.
Because you see the same kind of behavior
if you've got,
a turn array that's returning two different strings
of the same length and that kinda thing too.
Yes.
(audience mumbling)
- Oh, you're saying, ohh.
Leslie said it would decay to a pointer
because it's,
template argument by value.
I'm not, yeah.
I think that's right,
you actually have to specialize the pinpoint
based on the array.
Alright, anyhow, that's a side, okay.
So this is literally the most efficient thing possible.
This is my surprise,
this is how I wanted to phrase this.
Standard array has zero constructors for efficiency!
Just let that sink in.
If you've seen any of the talks,
from Neiko, about like an initialization of trivial classes,
and he goes through great length to explain
how you need all of the different constructors
to get things as efficient at possible.
I'm not saying you go back to your job
and start writing all of your classes
with all of your members public
and don't have any constructors.
I'm just saying,
this.
(audience laughing)
Alright.
On to something different.
Where ever my water went.
What do we have?
I think by now we have the rules.
A new slide comes up,
you tell me what happens.
Okay, so
we have get_s, which is create an object of type S.
Which is then immediately calling
an object of type get_data
and then we're printing
or excuse me calling the function called get_data,
which is returning a vector events by const reference.
So, what's printed?
(audience mumbling)
One.
(audience mumbling)
Undefined,
unknown.
Alright, we can break this down,
it is unknown
because
if we have a range four loop,
this is literally what the standard says
that the compiler is doing.
(audience mumbling)
An auto ref ref--
(audience mumbling)
Yes.
Auto ref ref would extend the lifetime
of the thing returned by get_s.
Not the thing returned by get_data from get_s.
It is not recursive object lifetime extension.
It is unknown because we have a dangling reference
to the object of type_s.
Oh look, I even put dangling reference
on the slide.
It's like I knew what was coming up.
What warnings do we get from the compiler?
(audience mumbling)
Sorry, I get ahead of myself.
No, um, I'm pretty sure we're at,
yeah, no warnings with clang.
And no warnings with gcc.
Those are the compilers I have at my disposal
at the moment.
Yes.
(audience mumbling)
Oh.
That's (mumbles) doesn't warn,
is that one of the ne--
(audience mumbling)
Herb's new extension, okay.
(audience mumbling)
Well, it's interesting that
it's not correctly diagnosing this
and I will explain why on the next slide.
C++20's for-init.
This was added in C++20 specifically for this exact
kind of case, which I find is interesting
that the lifetime warnings aren't catching it then.
But, it's probably,
it's difficult.
These are difficult problems to catch
a static analysis time.
At least I have to assume they're difficult
because people aren't doing them
and I'd never written a static analyzer.
Okay.
So, C++20 lets us do this.
Now, just for the record
if you'd try this in your pre C++20 compiler,
you'll probably get some weird warning
about how your missing the last third part of your four loop
because now we're getting into a syntax that looks
like as far as the number of semicolons required
and stuff,
it looks pretty much like it's a regular four loop.
But we can see here that the compiler now
generates code like this for us.
It has a const
auto s.
We can see that line was just copied in from line 12
to underline 13.
And then it is using it's auto ref ref
on the range object.
Any questions?
(audience mumbling)
Yeah, go ahead, sorry.
(audience mumbling)
That was recursive reference on a initialization
not on return values.
That is the difference as far as the standard is concerned.
Wait, there was.
Yes.
(audience mumbling)
Um, well if I had done auto ref s,
I would have recreated the exact same problem.
I would have created,
oh no, it would be const auto.
If I had done const auto ref,
that would have been allowed here.
But it would have been,
exactly the same thing.
We still would have had exactly one object
of type_s created,
it's just whether or not you use
the const reference lifetime extension rules
or you used return value optimization.
Yes.
(audience mumbling)
Yes.
No, no, wait, just for the record.
I did not suggest having no constructors.
(audience laughing)
(audience mumbling)
Yes, if I had not had a function called get_data here,
it would have worked fine.
That is correct.
It actually took me awhile to carefully craft
these slides to make my point.
That's,
that's like a little secret.
People spend a lot of time trying to get
the exact example that they want you to comment on.
Alright.
So, I ranged four loops,
create these hidden variables for you
that have their own questions of lifetime
that we have to be aware of.
Alright, time.
I am moving too slowly.
Okay.
What warning might be get from this code?
(audience mumbling)
Shadow.
Shadow warning.
We can break it down,
and ill start to move a little bit faster.
X shadows a previous declaration of x.
So, this is the code and we will expand it out
one block at a time.
So, this, auto const auto x equals get_val,
it says if we had done this, we created a new scope.
I know the first time I presented this
people missed the scope that was created on line five.
That is not the opening curly for main,
that is a new explicit scope.
So there's a new scope where that x is created.
And then we break it down further,
we've got our inner x, so that's the one that shadows.
So our surprise here if-init statements are visible
for the else blocks as well.
I find teaching this
that some people just don't expect that.
It's often the first question people ask
about C++17 if-init stuff.
Okay.
This is
similar to these things.
What is printed here?
(audience mumbling)
S ( ~ s.
Now, what is printed?
I've got
(audience mumbling)
s ( ~ s.
Okay, who says same thing?
Who says something more?
Alright,
everyone who voted said same thing.
So, I like to point out,
this is required in C++17
but every compiler I've gone back to 1995 does this.
It's
it's been a thing for a very long time.
(audience mumbling)
The mechanism changed depending on your ABI.
(audience mumbling)
Yes.
As of the, what itanieum IBA
if I'm being technically correct.
(audience mumbling)
Yes.
Well,
(audience mumbling)
okay, well, you know what
I'm almost out of time,
Gasper's arguing that it's not an optimization
because it's required.
I would like to point out the mechanism
because it actually works on most ABIs.
Is the object, little s, on line 16,
it's memory is allocated right here.
A pointer to it is passed forward,
into get_other_s, which is passed forward into get_s,
which is initialized.
That object never moved.
From a compiler standpoint,
there was nothing to optimize.
It was always right there.
Yes, Ben.
(audience mumbling)
Yes, prior to 17, you still had to have the copy instructor
even though it was never going to call it.
Yes, which is a little annoying.
But, now, you don't have to.
(audience mumbling)
You're right, so yes.
It's not longer an optimization,
it's not under the (mumbles) rule.
Alright.
RVO is super awesome, or illision, whatever.
Same,
(audience mumbling)
dag gummit.
Make me change my slides on the fly
because
if I did this,
what changes?
(audience mumbling)
Yes.
The answer was now it is NRVO.
Named returned value optimization,
I believe I have that correct.
It is,
it has another letter.
We are going to get the exact same output from the compiler.
It's good at these things.
So,
it's easy
and I don't like Copyalison,
I don't like the frays, Copyalsion.
I also don't like return value optimizations
since everyone's done it for 30 years.
We shall move on.
Okay.
Where'd my controller go?
Alright.
This is a little bit more complicated now.
I've got this thing called a holder.
I am constructing a holder with my function on line 15
called get_Holder.
It is initializing the return value
and then I'm calling dot_s.
So, I'm initializing my local variable s
on line 16,
with this thing that was returned from a temporary.
And then I'm returning that from the function
and then on line 21 I'm initializing this.
Alright,
what's printed here?
(audience mumbling)
S (.
Alright, what's the first thing that's printed?
S (, let's get that out.
(mumbles)
what's the next thing that's printed?
(audience mumbling)
what's that?
(audience mumbling)
Move, did you say move constructor?
Copy constructor.
Okay.
Who says copy constructor?
Who says move constructor?
Okay, no one says move constructor.
It is move constructor.
It is the move constructor
because the thing returned from get_Holder
is an R value, therefore,
it is a perfect candidate for calling the move constructor.
That's the simple way of wording this.
So,
on line 21,
nothing is printed,
because as we said, that object little s on line 21,
is actually like it's memory was allocated here
and it was passed forward, and the things.
Okay,
so we have an s, a move constructor,
and a destructor and destructor.
So, moves happen automatically with r-values.
Don't try to help the compiler.
I've changed this to a structured binding.
Now what's printed?
Someone says the same thing?
Who says move constructor?
Now?
Oh, now no one wants
to raise their hands for move constructor.
Who says copy constructor?
Okay, the rest of you have just given up.
Okay,
you're saying copy constructor.
It is a copy constructor
because
the compiler says that this code
was magically created for us.
This object s, that we thought was a local object
is actually a reference to the temporary
that was returned by the get_Holder function.
(audience mumbling)
No,
(audience mumbling)
it is not a referenced, it is a
it is, yes,
an alias.
It is, no sorry.
Which one are we referring to?
(audience mumbling)
No, no that's definitely a reference.
Line 17 is definitely a reference.
That is, you can look up
the wording of
structured bindings and see these kinds of examples.
(audience mumbling)
I have had conservations with people
involved in the standard
that say we can't tear apart objects
so, no, currently, the as if thing can't do things here.
But, that is outside of my depth at the moment.
Okay, so our structured bindings mess with our
ability to get our moves.
So, in this code,
is the destructor called?
We are creating an object on line 12.
Do we have any reason to believe
the destructor of s is not called?
It is not trivially destructible.
It has a destructor right there that puts.
So, we expect to see ~ s printed?
(audience mumbling)
Okay.
Now, I have put a throw statement in my constructor.
Do we see the constructor called?
(audience mumbling)
Okay, so I'm getting yes, no, no, yes, yes, no, no, yes.
Who says yes it is.
I hope I didn't need that.
I don't know what that is.
Okay.
Who says yes the destructor is called?
Okay, who says no it's not called.
The nos, the nos have more hands,
so therefore it's the correct answer.
(audience laughing)
This constructor call did not complete.
The object's lifetime has not begun
because it's constructor has not completed.
(audience mumbling)
(mumbles)
Shhh, (clearing throat)
okay.
I have added a call to the delegating,
I've added a delegating constructor call.
Is the destructor called now?
(audience mumbling)
okay, so I get some emphatic yes's from the front row.
Yes, it has been called, why?
(audience mumbling)
Yes, effectively when the first destructor exits
is when the object's lifetime has begun.
So, when this default constructor on line seven
is completed, the object's lifetime has been
considered to be have begun,
therefore, it's destructor will be called.
Now, if you really want to have fun
when you want to play with this later,
have a bunch of random sub-objects
that throw destructors at different points in construction
and play with what gets printed
and learn all kinds of things
about how much work the compiler actually has to do
to keep track of which objects properly
had their lifetime begin
so that it knows which destructors to call.
And then, thank your compiler developer
for getting it all correct, probably.
(audience laughing)
As far as I know, there's still an open bug
for something about this in gcc that's like old
and I forget the details though.
Alright, so once the delegating constructor completes,
the object lifetime has begun.
So, this is from a, I should have had a link to it,
a rather popular post by Howard Hnnant
on stack overflow.
And this is the kind of example he made here
is that we can do interesting things
to make sure that our
sub-objects lifetimes
are managed correctly.
So, if one of them were to throw an exception.
Like if the line eight were to throw an exception,
we know that the object that was allocated on line seven
would be properly cleaned up.
This is valid defined behavior.
However, I put it in really big letters.
I hope you can read it.
Don't do this at home, use unique_ptr.
This is, this is, violates all kinds of
like never call new, never call delete,
don't use raw pointers.
Don't manage the lifetime of more than one object.
Yes, all kinds of best practices are violated here.
So, an object lifetime has begun
after any deconstructor is completed.
This is my bold title conclusion slide.
Anywhere where the specs say the compiler
transformed the code for you,
there might be a surprise.
If you get bored and you're reading through the standard
you will see some of these things.
Avoiding these issues.
We need to think about lifetime.
We have our delegating constructor,
it's just a waste of code, right.
We can just return the first element
if that's really what we wanted to do.
So, don't name temporaries.
This helps us think about object lifetime correctly.
If we don't have a name,
we don't have to worry about it, pretty much.
This is a thought.
I haven't tested this in real life yet.
Consider requiring that all your structured bindings
actually be by reference.
Because this then communicates
that you are actually dealing with references here.
You're going to get the exact same results
but now you have this note effectively in your code.
I don't know.
Who likes that idea?
A hand full of people.
Who does not like that idea?
(audience mumbling)
(audience laughing)
I will not repeat that comment.
(laughs)
Using our tools.
Warn all the things, thanks Ben.
Wshadow catches some of these things.
Clang-tidy core guideline checkers would catch
many of these things.
The good warnings and our guideline checkers
check all, any implicit conversion from an array
to a pointer is caught.
I love that.
Sanitizers, every single one of these examples
is trivially caught by a sanitizer.
Are you using sanitizers on your code?
(audience mumbling)
Yes?
If you're not, when you leave this session
go implement sanitizers in your
continuous integration environment,
which you're all using, right?
Yeah, everyone has a CI of some sort?
Alright.
So, be careful with initializer_list.
Effectively, it was really only intended for
trivial literal types,
keep that in mind.
Oh, oh yeah, I can't skip this.
Constexpr all the things.
This is invalid d reference, we know this right?
I have like two minutes left.
So,
bam!
What does this code do?
(audience mumbling)
It fails to compile, why?
(audience mumbling)
Because it doesn't allow undefined behavior,
it says it right there on the slide.
Good job.
Okay, taking this example, where we have this
where C++20 is four initializer_list,
four initializer is supposed to help save us here.
This
this example caught by a
would be caught by a sanitizer.
If we make the context perversion of it
we're in the exact same place.
Compiler will not compile it
because it see undefined behavior.
I can actually,
execute it, or compile it real quick.
We get a constexpr variable
is not initialized by constexpr.
And I love the warnings that you now get
in a constexpr context that you can't get
from basically any other tool.
Read of object outside it's lifetime is not allowed.
What!
(audience mumbling)
An a constant expression.
(audience laughing)
But it's not allowed in general.
If you can build your code in such a way
that you can do constexpr testing
you're going to catch many more potential bugs.
Similarly, string view is constexpr enabled.
We can do this, this version now refuses to compile.
So, yes,
I have 42 seconds left.
Yes!
I'll leave that up.
Any questions?
Quick escape!
Um, any questions?
Yes?
(audience mumbling)
Do I know the rationale for the,
if the init variable is visible in the l statement?
Once you actually get used to using the if-inits,
it really is the only thing that makes sense.
Because you're going to initialize the thing
and then you might want to take two different
two different actions on that object
depending on whether or not
it was some other thing evaluated (mumbles).
It really is the only thing that makes sense.
But it's,
you're not necessarily thinking about it
when you go to use it.
Anything else?
Okay, so, a question way,
you might have to run up to the microphone or something
because, there's one right here.
Run faster!
You have negative minutes left.
Yes.
- [Audience Member] I have a question about,
well I think
the NRVO portion got skipped
quite a bit because the compiler can't possibly
get all the cases of NRVO unless
if you know that there is a unique source.
= Right, - [Audience Member] So,
- Yes, well if you think about that
that pointer that I said
that object that's created here
and then a pointer is passed forward to it,
it clearly, that pointer can only be in one place.
It must only be used to initialize one thing.
And you're right, if you have multiple
well, if you have multiple,
- [Audience Member] Return statements.
- Return statements,
with named objects,
then yes.
It can't do the return value optimization the same way.
One of them will get it
and the other one will get a copy or whatever, or move.
Yeah. - [Audience Member] Sure,
I mean,
I just think if you're depending on NRVO,
your code's gonna get very brittle.
- Oh, yes, if you're depending, oh okay.
Well, I don't need to repeat it
because you're on the microphone.
I agree
which is why I also am pretty strong
on this don't name temporaries
if you don't have a reason to.
I try really hard to not name the things
that I'm returning from functions, personally.
And it makes the code much more modular
and makes my functions shorter,
and I'm happier in general.
Anything else?
I know it's time to go,
I don't want to keep anyone over who needs to like,
do whatever, eat dinner, or something.
Alright, thank you.
(clapping)