In this video, we'll learn about c++'s move constructor
and move semantics.
These features were introduced in c++11,
and they can be difficult to understand. So we'll take it slow and one step at a time.
While move semantics is not considered a beginning c++ topic,
it's important to understand it conceptually
so you can identify it in code when you see it.
One of c++11's most pervasive features was probably move semantics.
And to really understand move semantics, it's important to be able to tell
whether an expression is an l-value or an r-value.
If you need a refresher about L-values and R-values,
please refer to the L-values and R-values video in section 12 of this course.
But a useful rule of thumb is that when you can refer to an object by name
or you can follow a pointer to get to an object,
then that object is addressable and it's an L-value,
and R-values are everything else.
But in the context of move semantics
r-values refer to temporary objects that are created by the compiler
and objects returned from methods.
As you can see in this slide, when the expression 100 plus 200 is evaluated,
the compiler stores 300 in an unnamed temporary value.
This value is not addressable so it's an R-value.
The R-value is then assigned to the L-value total.
The same happens with objects. However,
with objects there can be a great amount of overhead
if copy constructors are called over and over and over again,
making copies of these temporary objects.
And when we have raw pointers and we have to do deep copies,
then the overhead is even greater.
This is where move semantics and the move constructor comes into the picture.
R-value objects are the objects that move semantics addresses.
We know that copy constructors are called whenever the compiler needs to make a copy of an object.
We also know that if our class contains a raw pointer,
then we must implement deep copy.
However, this can be computationally expensive
since we have to allocate space for the copy and then copy the data over.
The c++ move constructor
moves the object rather than copies it, this can be extremely efficient.
Move constructors are optional. If you don't provide them, then the copy constructors will be called.
But it's a good idea to use move constructors for efficiency.
Finally, when you run your code and step through it in the debugger,
you might not see move constructors being called.
In fact sometimes you may not even see the copy constructors being called.
If you experience this, it's probably due to something called copy illusion.
Copy illusion is a compiler optimization technique
that eliminates unnecessary copying.
Compilers are really smart with their optimizations now.
And one of the common techniques is called return value optimization,
that's when the compiler generates code that doesn't create a copy every
return value from a function making the code much more efficient.
So let's start by talking about R-value references.
We've already seen L-value references which are references to L-values.
Well, R-value references are references to R-values.
In the context of move semantics, think of R-value references
as references to those temporary objects that we were talking about.
L-value references are declared using a single ampersand like we've done,
but r-value references are declared using two ampersands.
Let's see some examples.
In this example, we'll see the difference between an L-values reference and an
R-value reference.
First, we declare x as an integer and initialize it to a 100.
In this case, x is an L-value, it's addressable, and it's got a name.
Now we can create r ref as a reference to x.
And since x is an L-value, this is an L-values reference.
We can use l ref to refer to x. Nothing new, this is stuff we've seen already.
But now let's see what an R-value reference looks like.
R ref is declared as an R-value ref to an integer
with a double ampersand operator,
and we set it to refer to 200.
Since 200 is an R-value,
this makes ref an R-value reference.
Now we can actually assign 300 to r ref,
which stores 300 into the R-value
of where ref refers to. In other words, we just change that temporary variable.
Notice that if we try to assign an L-value to an R-value reference,
we get a compiler error, which makes perfect sense.
So now let's look at L-value references in the context of function parameters.
This is what we've been doing all along up to this point in the course.
We have an integer x, we initialize it to 100,
x is an L-values.
Now look at the function func.
It expects an L-value reference, as denoted by the single ampersand.
So it wants an L-value.
If we call func and pass in x,
then this is okay since x is an L-value
and num will be the reference to an L-value.
But what happens if we call func with 200.
Well, 200 is not an L-value, it's an R-value,
and the compiler will generate an error.
You can see the error message below that the compiler generates.
Now let's look at R-value references in the context of function parameters.
We, again, have defined x to be an integer and initialized it to 100.
This time we declare func as a function that expects an R-value reference,
and we denote that with the double ampersands.
So it wants an R-value.
If we call func and pass in 200,
this is okay since 200 is an R-value.
And now num will be a reference to an R-value.
But what happens if we call func with x? Well, we get an error message.
X is not an R-value, it's an L-value.
So the compiler generates the error, and you can see the message it generates below.
Now we can overload these functions and have both of them in our code at
the same time since they have unique signatures.
And the compiler will call the correct function
depending on whether the parameter is an L-value or and R-value.
We're going to take advantage of this when we define the move constructor.
So now let's see how this works with a move constructor in a class.
Here, I've created a class named move and it has a raw pointer as a data member.
This class is similar to the shallow and deep classes we used earlier in this section.
The rest of the class methods are pretty standard, and we've seen them before.
Notice that we've included a copy constructor that does a deep copy.
We have not defined the move constructor yet, we'll do that soon.
But first, let's look at the copy constructor.
Here's the code for the copy constructor, again, pretty standard deep copy.
We allocate storage, and then we copy the data.
Now we run a simple main that creates a vector of some move objects,
and we call the vector vec, and we push back to move object.
Now notice what's happening in the argument list.
Move 10 and move 20 are creating temporary objects, they're unnamed.
So they're going to be R-values.
What happens is that the compiler is going to use the copy constructor
to make copies of these guys.
So here's the sample output from that program.
You can see these constructors and destructors and copy constructors being called.
In fact, you can see that the copy constructor is doing several deep
copies, which can be inefficient.
Now let's add the move constructor to the class to make it much more efficient.
So what does the move constructor actually do.
Well, instead of making a deep copy like the copy constructor does.
Well, instead of making a deep copy as the copy constructor does,
it simply moves the resource that's on the heap.
And the way we do that is just to assign the pointer from the source object to the current object.
You might be thinking isn't that what default member-wise copy is.
And yes, it is.
But now the difference is that we know out the pointer in the source object.
So what we have now is an object
who owns the data on the heap and then the original object with a null pointer to that data.
This is real efficient since we're not doing a copy at all.
Before we implement the move constructor, let's look at its syntax.
As you can see it's very similar to the syntax for a copy constructor,
but it's got two differences.
First, there's no const qualifier for the parameter source.
There can't be because we need to modify it in order to null out its pointer.
And second, the parameter is an r-value reference,
and you can see that with the double ampersands.
You can see in the last two declarations what the move constructor
prototype might look like for a player class and the move class.
So here we're adding the move constructor prototype to the move class declaration,
and now we need to implement it.
Notice how simple the implementation is.
We simply copy source.data to data.
That's a simple copy of a pointer data member.
Again, we're not copying what it's pointing to, we're not doing a deep copy.
Then we null out source.data.
This step is really important.
Otherwise, we end up with the equivalent of a shallow copy and not a move.
That's it. Many resources that describe
the move constructor say that it steals or pilfers the data
and then it nulls out the source pointer,
and that's really a good way to think about it, that's exactly what it's doing.
Now we've got our same program.
And when we execute it, instead of the copy constructors being called for those
temporary objects, the move constructors will be called because there are values.
And it's going to be much more efficient than doing deep copies.
So let's take a look at the output now.
As you can see, there are no copy constructor calls at all
and the move constructor is indeed being called.
You can also see when the destructors are called for null pointer.
That's destroying the object that we just moved and set its data pointer to null.
So there you go. That's a basic introduction to R-value references in the move constructor.
I know it's complicated. You'll probably have to watch this video a few times, but don't worry
everybody goes through this. It's a difficult topic to understand.
We'll see move semantics again in the next section of the course
when we overload the assignment operator to do both copy and move.
There's so much more to move semantics in the move constructor that are
better left for a more advanced course,
but now you know the foundations and you'll be fine when you learn about them.
So let's head over to the ide and run this program and see it in action.
Okay. So I'm in the IDE. Again, I'm in the section 13 workspace
in the MoveConstructor project.
And what I've got here is that class move that we used in the slides,
it's got a raw pointer to an integer,
it's got get set methods and your typical constructor,
a copy constructor destructor. Now you'll notice that I've got a move
constructor here but I've commented it out because I'm going to run this first
with only the copy constructor so you can see what's going on.
And right here is the copy constructor on line 17.
Here's my implemented copy constructor.
Again, I've commented out the move constructor will uncomment that at a moment.
And here's my destructor.
My destructor looks a little different because
if my data is not equal to the null pointer,
then I'm just saying what I'm destroying. Otherwise, I'm just saying that I'm destroying null pointer.
And that's important because it will let us know if we are
destroying those objects that we just moved,
and it'll be really easy to see when it says null pointer.
Okay. So here's my main. I'm creating a vector of move objects,
and I'm simply calling pushback,
and I'm supplying move in here.Now you'll notice
there's no variable name there, right. So that's going to create a temporary object.
And a temporary object is an R-value. So in this case,
it's going to use the copy constructor. Once we put the move constructor in, it's going to use that.
So let's walk through this example. And you'll see what's happening.
So I'll start the debugger
and I'll move the window over here to the right.
And this is where we're at, right here.
So first thing we need to do is we need to construct this object,
right.We need to create that temporary object, and we're going to use the constructor to do that. So we'll
step through here. You can see the constructor being called here.
It's going to allocate that object on the heap
and store whatever we passed it into it.
We're going to display constructor for 10.
And now we're in the copy constructor because remember we just
created this temporary, right. So this temporary now needs to be copied into the vector.
So we're in the copy constructor.
We're here. We're going to construct a copy.
So we're going right back here. We're delegating to the constructor.
We're going to create another 10.
And it says here, we just deep copied that 10, which is exactly what we did.
Now we're back here, and we need to destroy that copy we made.
So the destructor will be called right now.
You can see a data will be pointing to 10.
And we're going to destroy that copy of 10.
That's it. That's pretty much exactly what's happening.
You can see that we're using copy constructors just like we've used all the time.
Now the fact that this is an R-value,
that comes into play when we use the move constructor.
Now I'm going to stop this, and I'm going to uncomment out
all of these here. So you can see what's happening, it might be a little surprising.
So what I've done is I've pushed back
10 20 all the way through 80. Remember, these are all creating temporary objects,
so they're going to be copied and copy constructed
so that the vector can push them back. Remember, the vector
makes copies of these guys.So it's going to make a copy of the temp.
That's one of the problems. That's why it's so inefficient.
But it's even worse than that because if we run this
without debugging it. Let's just build and run.
Let's take a look at what we just got here. I'm going to scroll all the way to the top.
I'm at the top now.
So you can see that I've created that 10, right. Then I've copied it. I've done a deep copy,
and then I'm destroying it, I'm doing the same thing for 20.
So you can see all these deep copies. But if you count them, you've got 1 2 3
4 5 6 7 8 9 10,
about 15 deep copies.
That's much more than you would expect, right. When you look at the code,
I don't think you would expect that many.
l mean there's only 8 pushbacks here.
But what's going on is that the vector
is actually growing behind the scenes.
And when the vector grows, those objects need to be copied to the new storage.
Remember, the vector is using contiguous storage.
So it's making copies of the objects behind the scenes as well.
So you can see that a lot of times there's a lot of copying going on behind the scenes that we're not aware of.
So that's the version with the copy constructor.
Now what we'll do is we'll uncomment out
the move constructor.
And you can see what the move constructor is doing. The same thing that we did on the slides.
The move constructor simply copying the source data and nulling out the pointer.
Really, really straightforward.
And I'll uncomment out the prototype here.
Okay. Now what we'll do is we'll debug this again,
and we'll walk through that very first one so you can see what's happening.
So I'm right here on line 55.
I need to create that move object, right. That's a regular constructor to create that,
that creates that temporary R-value.
So let's walk through there.
I'm going to create that 10
object that 10 move object. Now notice what happened?
The move constructor got called.
That's the move constructor right there with the double ampersand.
So we got called rather than the copy constructor. Why? Because this is an R-value temporary.
Now what we're going to do is we're basically going to steal the data
and null out the pointer to the source objects, right. So we're going to move it rather than copy it.
So we're here we're sourcing. We're nulling out the source data,
and you can see the move constructor got called.
Now look what happens when I go to delete that object right the one that we just moved the source.
We're here in the destructor, just like we would expect.
But if you look at the object we're destroying.
Notice it's a null. The data's a null. That's the one we just nulled out, right.
So in this case, I'm simply
saying destructor freeing data for
null pointer.
So that's really obvious, which one is being destroyed.
And then at this point, I'm back here.
Okay. So you can hopefully see the difference here. How much more efficient this is.
And I think you'll really see the difference in efficiency when we just run it without the debugger.
So let me just run that without the debugger.
And you can see here again I'll scroll all the way to the top.
You can see that as you scroll down here, there is not one single copy
constructor being called. So we're doing no deep copies,
we're only moving resources using the move constructor.
Okay. So there you go.
That's a introduction to move constructors and move semantics.
And I think this is a pretty good example that you can walk through and understand what's going on.