CHRIS BLUME: So
C++ move semantics.
This is new as of C++ '11.
Just a quick little background.
C++ is an evolving language.
The standard changes over time.
So sometimes, you'll hear people
will say C++ '98, or 2003,
or 2011, '14, '17, so on.
That's as a new
standard comes out
that's adding things or
removing things, updating.
So anyway, as of C++ '11,
there's a concept of moving.
So here's an example where we
have this function, get_name().
It returns a string, Chris.
And then we call
presenter.set_name() with
the result of get_name().
In old-style C++, we would
consider that temporary.
And so the string that's
being returned-- it
doesn't have a variable it's
really being stored into.
It just kind of gets silently
created and then passed along
and silently destroyed,
hence the temporary name.
But that means that
we're going to be making
lots of these
temporaries-- constructing
them, and destructing them,
making copies of them.
That's a little bit wasteful.
Especially if we
know the thing is
about to be destroyed anyway,
then why create a new thing,
destroy it?
It's kind of silly.
So for a string, you can imagine
there's the temporary string
and it points to the
buffer that's actually
holding the characters.
And then when we call
presenter.set_name(),
maybe that's a separate
variable, a string,
and then we copy the
buffer into a new buffer.
It also contains the
characters Chris.
That's where we kind of
lose this efficiency, where
we now have two buffers
and we're copying
all the characters over.
So move semantics are just this.
It says, well, I
know the temporary's
about to be destroyed anyway, so
what if I just kind of sneakily
steal its buffer?
And then I'll set
its pointer to null,
so that when it
tries to clean up,
there's nothing
for it to clean up.
And that way, it can go
ahead and get destroyed,
and it doesn't
matter, and I didn't
have to make a new buffer and
copy every single character
over there.
That's all move semantics is.
It gives you the option
to know if a thing is
about to be destroyed and it's
safe to steal buffers from it.
So this is the old-style copy
constructor and copy assignment
operator.
And this is what we're used to.
The variable that's passed
in is a const reference.
In C++ '11, with the move
semantics, we've added this.
It's a move constructor and
a move assignment operator.
There's two main
differences here.
One is there's a double
ampersand instead
of a regular ampersand.
That indicates that the
variable that was passed in
is an rvalue reference,
and that just
means it's the thing
about to be destroyed.
The other change
is it's not const.
And the reason it's
not const is because we
need to be able to update
it to change its pointer
to point to null.
That way, when it gets
destroyed, everything's fine.
So here we're creating
a variable named name.
We're storing a string in it.
And then we're calling
presenter.set_name().
And we're trying to
pass in this name,
but we want to move it somehow.
If we did the old pass it
in, it would make a copy.
And so what we can do
is just sort of static
cast the variable to--
there's that double
ampersand-- so we're casting it
to an rvalue reference.
It's kind of like we're
telling the compiler,
I'm not going to
use this anymore.
The thing's about
to be destroyed.
It's OK to go ahead
and steal my buffer.
So that's one way to do it.
But that's kind of
difficult to read.
It's kind of a mess.
So that's where
standard move comes in.
It's the exact same thing.
All it does is cast your
variable to an rvalue reference
so that we know we can
steal the buffer from you.
So here we have two versions
of the set_name() function.
The first one is the
old style we're used to.
It takes a const reference.
The second one takes
an rvalue reference.
This is where we actually
kind of ran into a problem.
C++, as a community, we got
really confused and we said,
we actually don't know how
to write functions anymore
because these move semantics
make us overload like this.
So this is only one parameter.
Now we have to
have two functions.
Imagine if somebody
reports a bug,
I go fix the bug in
one of the functions,
and I forget to fix the
bug in the other function.
That's no good.
But if it was more
than one parameter,
if it was two parameters,
I need to have
four functions because it
could be lvalue, lvalue;
rvalue, rvalue; rvalue,
rvalue; rvalue, lvalue.
So you have this
combinatorial explosion
as more and more parameters
become potentially
in sync parameters
from rvalue references.
It just gets
completely out of hand.
And the whole C++ community
was just like, what do we do?
We're lost.
So we all kind of agreed on a
solution that is not perfect,
but it's as close to perfect
as we're going to get.
We do this.
And if you are used
to old-style C++,
your brain is
probably going, no.
That's a code smell.
You're not supposed to do
that because that's going
to make a copy of the string.
And it's true.
But it'll also make
a move of the string.
So essentially, when this
function gets called,
it has to construct a name.
And there's a lot of ways
it could construct a name.
It could copy construct it
or it can move construct it.
That means this
one function covers
both of those last functions.
So now we don't have this
combinatorial explosion where
you have multiple functions.
Don't have to worry
about that anymore.
And if we move constructed
it, it's no big deal.
Moves are supposed to be cheap.
We're just changing pointers.
But if we copy constructed,
well, then, yeah.
We had to do one extra copy.
That's worth it to not have
four different functions.
So that's the trade-off
everybody sort of agreed upon,
which means with new-style C++,
we have to unlearn all our old
habits and learn
these new habits.
So here I want to just
imagine this scenario.
We're not supposed to write
code like this at Google,
but I want to explain a thing.
So here we have that function.
And we're taking an
rvalue reference,
and we want to pass
it further down.
So that's when we
would do this move.
But there's something
a little weird.
It was already an
rvalue reference,
so I kind of don't
need to move it, right?
Well, another, like, we
have to unlearn old habits.
Even though it looks
like the parameter is
an rvalue reference, it's not.
That just means this function
binds to an rvalue reference.
But once we're
inside the function,
it's an lvalue reference.
It's the old-style
reference we're used to.
So that type is what it
binds to, not what it is.
And then we have to cast
our value reference back
to an rvalue reference.
So that probably got
really confusing.
So let me try to clear
this up a little bit.
Let's see.
Do I have notes on this?
Oh, shoot.
I don't.
What's the next one?
I'm going to skip this.
I'm going to come back.
It was confusing.
Oh, I remember what
I was going to say.
Phew.
So the easy way to
know if something
is an lvalue or an rvalue
is if it has a name,
if it has a place
to live in memory,
if it's a variable somewhere.
So you remember
earlier the example
where we return something
that didn't have a name.
It didn't have a variable
place to live in memory.
So that would be an rvalue.
But here, it has a name.
It has a variable.
It has a place to
live in memory.
That's why it's an lvalue.
Now, this is 99% true.
There is a version that's
100% true I could tell you,
but it's way more complicated.
So I think everybody's happier
just learning if it has a name,
it's an lvalue.
Even if this says it's
an rvalue, it's not.
That's why we had
to cast it back.
So I think that makes
it less scary, right?
I'm seeing some nods.
I'll take that.
So like I said, in
Google, we don't actually
write this sort of thing.
We only write the move
constructor and the move
assignment operator.
So having a different
function like this
and then taking an
rvalue inside it?
That's kind of a no-no.
So at Google, we would write
it like this, and the same
in Chrome, obviously.
So that's all of move semantics.
So I'm going to jump
back in time now, way,
way back to slide 1.
Question in the back.
AUDIENCE: When would it be worth
it to use a holding reference
and then check [INAUDIBLE]
string from the [INAUDIBLE]??
CHRIS BLUME: That was
a really good question.
The question was about, when
is it appropriate to use
a forwarding reference?
So that's very complicated.
In this slide, I'm passing an
rvalue reference to a string,
but let's say that
function is templated.
So it's like template
type named t,
and instead of taking a string,
it's taking a t ref ref.
All of a sudden,
all the rules are
out the window
and it's something
completely new because t
ref ref binds to everything.
It binds to an lvalue,
an rvalue reference--
everything.
So what that means is
this standard move--
remember, that's exactly
just a static cast.
It forces it to be
an rvalue reference.
But what if the thing
that was passed to us
wasn't an rvalue reference?
We shouldn't really cast
it, because it's not
about to be destroyed.
That's where a forwarding
reference comes in.
So that says, if it was
an rvalue reference,
go ahead and cast it back
to an rvalue reference
and keep forwarding that on.
If it was an lvalue
reference or anything else,
don't cast it to an
rvalue, because we
don't want to destroy the
thing and steal its buffer,
and pass it on as it
should have been passed on.
AUDIENCE: [INAUDIBLE]
CHRIS BLUME: OK.
That made sense?
Question?
AUDIENCE: So in what
scenarios will the compiler
convert an lvalue to an rvalue
if you haven't specifically
[INAUDIBLE]?
CHRIS BLUME: The
question was, when
can the compiler convert an
lvalue to an rvalue on its own.
I don't think the
compiler ever will.
So in the original example,
where we just return a thing,
that was already an
rvalue reference.
So I don't think the compiler
can promote an rvalue
to an lvalue, I think.
Good question.
Yes?
AUDIENCE: Can you kind of
shed some light about that?
When it should do it, if
it should never do it,
stuff like that?
CHRIS BLUME: Sure.
So the question was returning
an rvalue reference.
And if I, say, like return
thing, and then I go, you know,
I could have just moved
this because I know it's
about to be destroyed anyway.
So instead, return
standard move thing,
And now the compiler
says, wait, wait, wait.
I was going to ellide that
copy, and I can't anymore
because you told me to move.
So what ends up happening is,
first things first, it already
was an rvalue reference because
the thing that's being returned
is about to be destroyed.
And so you didn't
actually have to move it.
But the compiler can
be smart and say,
here's the stack frame
that called our function,
and then here's the stack
inside our function.
Rather than put the thing
that I'm returning here
and then I have
to clean it up, I
know it's about to go
down here, so I'll just
work on the thing
in there and avoid
that copy in the first place.
So that's even
better than moving.
So that's why the
compiler says, even
though it was an
rvalue reference,
I'm going to treat it like
I was going to copy it,
and then I'm going
to skip the copy.
AUDIENCE: So regarding the
[INAUDIBLE] for ones like that,
we look at the example
that we can't [INAUDIBLE]..
CHRIS BLUME: Yes.
AUDIENCE: For me, this is
like a clear intention,
like, oh, I was given, I was
promised an rvalue coming in,
so I can move this and
feel good about it.
Right?
But now we remove this
because we can't use this.
Then how do I, at
the very least,
ensure that I'm not making
a copy, or how do I know,
other than the comment,
that I can do this?
I guess comments would be
the answer, but [INAUDIBLE]..
CHRIS BLUME: It's
a great question.
So the question was, this
provides a very nice guarantee
that the thing was an rvalue.
And if we go to what
we're forced to use,
we don't get that guarantee
anymore and that's unfortunate.
So how do I say it?
There's a mailing list of
people, like language lawyers,
at Google, and they're debating
what is the correct thing.
If you go back in
time, you'll actually
see me arguing
exactly that, and then
like two years later being
like, I've changed my mind.
[LAUGHTER]
So I was like really
bitter and pushing,
and then they're
just like, stop.
You're wrong.
And then I was like,
yeah, you guys were right.
So here's why.
By the way, it's important
that I should point this out.
You can do this, but it's
kind of like an exception
to the rule.
So the rule is don't do that,
but if you have a really good
reason, talk to some people.
Get owners' approval.
OK, fine.
But this is really saying,
I was the one being called.
I was the one who wrote the API.
I don't know what the
caller wants to do.
I don't know if they want
to make a copy or not.
And they can have had
the option either way.
Even if I wrote it this way,
they could have made a copy
and then moved the copy in.
But then that's putting
the burden on them,
and it makes the code
kind of unnatural,
whereas just calling standard
move is a lot more natural.
So the idea, as I
understand it, is this--
sorry, this-- allows
the caller flexibility.
They can do whatever
they want with your API,
and so now the burden is on you
to use it correctly, I guess.
But it's not locking the
caller into an awkward state.
So we do have a whole bunch of
stuff we can go back in time
and try to jump through.
Is everybody
satisfied with moving?
All right, sweet.
I'm going to have to rush
these slides, but that's fine.
The funny thing is, I
was telling Sam, oh, I'm
going to be short.
I don't need all this time.
I was so wrong.
OK.
Hi.
My name is Chris.
[LAUGHTER]
You can find me on social
media as ProgramMax.
Let's talk about C++ memory.
C++ didn't have a
memory model until 2011.
Thank you for coming
to my TEDTalk.
Subscribe, like, comment.
OK.
AUDIENCE: Mash that like button.
CHRIS BLUME: Yeah.
Bop the like button.
Ring the bell.
So I'm being a
little bit unfair.
It didn't need a memory model.
We had this other
thing, and here's why.
Prior to C++ '11, C++
didn't have threads.
And I know everyone's going
to like, yeah, it did.
I had threads in my program.
Yeah, the operating system
had threads, not C++.
The standard never
mentioned threads.
And so instead of a
proper memory model,
we used something
called sequence points.
You may not have heard
of sequence points,
but you're actually probably
familiar with the idea
of sequence points.
So imagine I have a equals
1, b equals 2, c equals 3.
The compiler can be like,
no, I can rearrange those
and you wouldn't know.
I can fiddle with
memory however I
want because there's
no observable side
effect if I fiddle with
these because I never
read the value of a.
So if I change it to like
a, c, b, It looks fine.
And that gives the optimizer
a lot of room to move.
It can do whatever it wants.
This is a good thing.
And that's why it's like,
as if it was sequential.
That was the promise,
and that's what
these sequence points are for.
But as soon as we introduced
threading, if it was a, c, b,
the other thread could
come in and say, OK, a
has been updated.
b hasn't been updated,
which means c shouldn't
have been updated, either.
Uh-oh.
It has been.
What's going on?
That's impossible in the code.
So that's why we're like, OK.
If we're going to
add threads, we
have to have a
proper memory model.
We can't use this sequence
point nonsense anymore.
So this is a
tautology I recognize,
but different hardware
works differently.
If we're trying to
define a memory model,
how do you say, well, all
memory behaves this way?
We can't, and it gets
really, really, really wild.
So I told you the example of
the a, b, c, in that order.
Even if the code
didn't get rearranged,
even if we wrote a, b, then
c, the CPU itself can be like,
well, I'm going to retire
this instruction first
and then that's being added
to the buffer of writes that
are pending, and
then those rights
can get flushed in
whatever order they want.
There's many, many steps
where these can be reordered.
So the reading thread
comes in and observes
something completely
different than what
the writing thread observed.
And it gets even
weirder than that.
Two reading threads could
observe different behavior,
different orders.
It's nonsense.
So trying to make a
memory model out of this
is just bonkers hard.
The way C++ did it is said, we
will do what the hardware does
and the bonkers hard stuff
is on you, the programmer.
You're welcome.
[LAUGHTER]
So we are on slide 5, and I've
already talked about hardware
and operating systems
in a C++ talk.
That's because the three
are intimately linked,
and the rest of the slides
are going to be like that.
They are going to
intertwine a lot.
So I want to talk
about memory mapping,
which is a kind of
unique thing to C++.
It's in other
low-level languages,
but it's not in
Java, for example.
So let's imagine we have
this hardware of ours.
Maybe it's like a GPS, or
a sound card, or something.
We could add this
on/off switch to it,
and that allows us to save
some power, save battery.
That's great.
How do I expose this hardware
on/off switch to code?
A driver wants to
disable is thing.
How do I let the driver do that?
That's where memory
mapping comes in.
So what we could do
is say, this hardware
is actually going to hijack
the memory arrange BBB to CCC.
So if you try to
access that memory,
you're not getting memory.
You're getting the
actual hardware.
And then we could say, OK,
well, at that memory address
BBB, the first byte,
maybe that is what
controls the on/off switch.
This is how we would
write the driver code.
So we have this volatile char
pointer-- that's just the byte.
And we get to set it to
the exact address we want.
And then we have turn
on, set it to true.
Turn off, set it to false.
That's easy.
Not so bad.
There's two things I
want to point out there.
One is we got to specify the
exact location in memory.
That's not available
in a lot of languages.
The other is that
volatile keyword.
So what that means is the memory
could change behind the scenes.
If we just read that
value, and then we
do some other
operations, and then
we go to read the value
again, the compiler
could say, oh, I know
the value hasn't changed.
We didn't change it.
But the hardware
maybe changed it.
So volatile is saying, don't
assume you can skip that read.
Read it again.
How am I doing on time?
Woo.
Alignment and pattern.
So we have an integer.
The size of the integer is
4 in the example I've used.
Natural alignments are
going to be multiples of 4--
0, 4, 8, yada, yada.
We have a char.
It's size 1.
Natural alignments
are multiples of 1--
0, 1, 2.
Easy.
Doesn't have to be this way,
but this is really common.
So now I have a class,
foo, and I have a char.
We know that's size 1.
And I have an int, size 4.
Uh-oh.
That didn't line
up on the 0, 4, 8
because that char kind of
shifted us over 1 byte.
So this is unaligned.
We'll get back to that.
So then does that mean that
the size of this whole class
is 5 bytes?
Marie's going, no.
You're right.
So if I say offset of
the member a, that's 0.
That means that the a,
the char, begins right
at the beginning of the class.
However, offset of b--
that integer-- is 4, not 1.
It was supposed to be
only right after the char,
but instead, it got bumped.
So what that means
is the compiler
actually did this for us.
We have our original char.
We have three extra
chars just to pad out.
And that puts our integer back
on a 4-byte alignment, so good.
We're aligned.
We don't even have to worry
about that whole unaligned
nonsense, and the size is 8.
This is cool.
Except it's kind of
uncool because what
was the size of my class?
I don't know.
Let's ask the compiler.
That's weird.
I should have a little control.
So let's talk about what
happens if it is unaligned.
So nowadays, processes
are pretty darn good,
and it's probably
not a big deal.
You can probably
do unaligned access
and it'll be just
fine in most cases.
But in some cases, it'll
be a little bit slower.
OK, that's not that bad.
But I have this one horror
story that I love so much,
I have to share it,
and I have time.
There is a PowerPC chip where
if you did an unaligned access,
the chip would actually fault,
and say, operating system,
if you want to try
to recover, fine.
But we have error
beyond recoverability.
But then this old
version of Mac OS
would go, well, you
know, an unaligned read?
That's not so bad.
What I could do, I know the
program was trying to read here
and it was unaligned,
so what I can do
is read the chunk before
and the chunk after.
Those are aligned.
Then I can sort of
shift things around
and I can fix what the
program was trying to do.
And then I can just resume
execution and everything's
going to be fine.
Imagine you're the
programmer, and you're like,
why is my program slow?
It's silently being slowed down.
You have no warning, no
indication of what's going on.
Then imagine you're reading
the documentation to the chip
because that's how
desperate you got.
[LAUGHTER]
And you're like, oh,
unaligned access would fault,
but we didn't fault,
so it can't be that.
So this is a nightmare.
Somebody figured it out
and I don't know how.
OK, so other weird
padding things.
Here we have class foo.
Starts with int, char, char.
So that looks like it should
be size 6, but it's 8.
I'll get to that later.
Don't worry.
But the end is aligned.
The char is aligned.
The char is aligned.
Everything seems fine.
But then this one.
I have a char, then
an int, then a char.
That made a bigger.
That's because it
had to align the int.
We know that much.
OK, no big deal.
But this should've been
size 6 and it was 8.
And this should be
char is 1, but then we
pad it out, so that's 4.
Add 4 to that for the
integer, we're at 8.
At the last char, should be 9.
How did we get to 12?
If you know why,
show me your hands
why this is 8, instead of 6.
OK, we have 3, 4, 5, 6?
OK.
Not that many people.
So imagine I have an array.
There's the first
element and then
there's the second element,
and the third, and so on.
That second element, if this was
size 6, on the second element,
that int wouldn't
have been aligned.
And so it actually
added some extra padding
here so that when you have
the second element following
the first one, the
int is still aligned.
And that's the exact same
reason why this thing is 12.
It had to add some
padding at the end
so that on the next
thing, it's aligned again.
So that means, not
only do we have padding
to line up this integer,
we have padding at the end
to line up the theoretical
second element if we ever
put it in an array.
It gets pretty messy.
Yes?
AUDIENCE: On some
architectures, the compiler's
going to do different
things depending
on what the alignment
requirements are
to the processor.
CHRIS BLUME: That's
totally correct.
So to echo that
on microphone, it
doesn't have to be an
array of the same thing.
It could have just been a
different type following it.
You're totally correct.
And different architectures,
different compilers,
going to do different things.
On the platform
I was working on,
an unaligned int access
is actually fine.
It's not a performance penalty.
So it didn't need to pad out the
first int, but it did anyway.
And I think it's
just like, well,
maybe he'll copy it to
an old machine, whatever.
There was a question somewhere.
AUDIENCE: [INAUDIBLE]
CHRIS BLUME: Yeah, don't
move to another machine.
Question?
AUDIENCE: If you embed foo
in another class that's not
[INAUDIBLE],, would the
compiler then put something
else in that padding place?
Like if you had another
field after foo [INAUDIBLE]??
CHRIS BLUME: Oh,
that's a good question.
I don't have an answer.
I'd have to check.
So the question was,
if foo, this class,
is a member of some other
class, rather than padding out
with empty bytes, could
it just use those bytes?
And I don't know.
Theoretically, it could.
Maybe, so long as things
are still aligned.
I don't know.
Question in the back.
AUDIENCE: On the
example on the right,
I'm assuming it's illegal for
the compiler to [INAUDIBLE]..
CHRIS BLUME: The question was,
on the example on the right,
why can't the compiler reorder
the fields to get this?
The reason why is
because our class
is kind of like a struct
in C, and that was actually
used to do memory layout.
And so if you start
rearranging things,
then it's like, well,
I thought two bytes
after my pointer
would be this variable
and it's somewhere else.
And so that's why.
[MUSIC PLAYING]
