Hello again!
In this video, we are going to look at move operators. Not to be confused with smooth operators.
That is something quite different!
So to recap, if we have a function which takes its argument by a const reference to lvalue, then
we can pass either lvalues or rvalues, when we call this function.
If we have an overload which takes the arguments by rvalue reference, and we pass a moveable rvalue,
then the compiler will call this one instead. So we can get different behaviour, depending on whether the
argument can be moved or not.
One situation where this could come in useful, is the copy constructor.
If you think back to our example, with a vector of 1 million strings, the copy constructor is going
to call the copy constructor for every one of those strings.
And that is going to create a lot of memory operations and data copying.
But if we could just move the data, then we could save all that work and get much better performance.
And in C++11, you can now do that.
There are two new special member functions for that purpose: the move constructor and the move assignment
operator.
And these will be called automatically if the object which is passed is a moveable rvalue.
These are overloads of the copy constructor for the cove constructor, and the assignment operator for
the move assignment operator.
These take their arguments by rvalue reference. And the copy assignment operator is now known as the
assignment copy operator,
to make clear we are talking about the version which takes an lvalue, and not the move assignment
operator, which takes a movable rvalue.
Let's look at the syntax for these.
So the copy constructor takes its argument by const reference to lvalue.
The Move constructor takes its argument by rvalue reference.
And similarly for the assignment operators. You will notice that the rvalue reference is not const.
That is because we are going to move from the argument.
So we are going to modify it, so it cannot be const.
The move operators are also declared as noexcept.
And there is actually three reasons why you should do that.
The first one is that generally move operators should just "move" data around.
They should not allocate new resources or do other things, which could throw exceptions.
The second one is, if you are halfway through a move operation, so you have some data in one object
and the rest of the data in another object. If an exception is thrown, there is no easy way to recover
from that.
It is pretty hard to unscramble an egg.
And the third reason is, if you want to put objects of your class inside one of the library containers,
then the move operators must be noexcept. Otherwise, the code will not compile. And the return value
from the assignment operators is the same.
These both return the modified object by lvalue reference.
And that will allow you to assign to it again.
We're going to add some move operators to a class now.
We are going to come back to our string-like class a bit later on in this section.
But first, we will start with something a bit simpler.
So we have a class which has a built in member, and a member which is a class object.
So what does it mean, to move an object of this class?
We need to move the int member, and the object member.
So if we are calling the move constructor, we need to call the move constructor
of these members.
So let's see what we get.
I have just put an empty shell for the class type, for the member.
If you like, you could put some member functions in here, and get them to print out when they are called.
So you can see what is happening.
Here is the Test
class, as we had it on the slide. It has a default constructor, so we can create objects of this class.
And you might like to think about why I need to put that in there.
The answer is, that
I have defined a copy constructor, so the compiler will not synthesize the default constructor. For the
copy constructor,
it takes its argument by lvalue reference to const.
Then it just calls the copy constructors for each member. With the move constructor,
it takes its argument by rvalue reference, and it is
noexcept. We can copy the built-in members. For the class object members, we need to call their move constructor.
This means we need to provide an rvalue as argument.
This member has a name, "m", and we can take its address.
So this is actually an lvalue.
So we need to convert this to an rvalue.
So we need to put it inside a call to move.
And then this will return the member as an rvalue. And that will call the move constructor for that
object.
For the assignment operator, now called the copy assignment operator.
We take the arguments by const reference to lvalue.
We check for self-assignment, and then we call the copy assignment operator for each of our members,
with the corresponding member from the argument object as the argument to that call.
And then we return the modified object, after it has been assigned to.
For the move assignment operator,
we take the argument by rvalue reference. It is declared
noexcept. We have the self-assignment check, and then we call the move assignment operator, for each
member.
So we call that for the built-in type. And for the object,
we need to provide an rvalue again.
So we need to call move(), to get an rvalue, and that will call the move assignment operator for this
class.
And then we return the modified object again.
In the main function, we create an object of this class.
Then we use the copy constructor.
This is going to be an lvalue.
So this should call the copy constructor.
Then we provide a temporary object, an rvalue.
So that should call the move constructor.
We have an lvalue which is cast to rvalue.
So that should call the move constructor again.
Then we do some assignments.
We use an lvalue object for the assignment.
So that should call the copy assignment operator.
And then we use a temporary object, an rvalue.
So that should call the move assignment operator.
So let's see what we get.
So this one with the lvalue calls the copy constructor.
This one seems to have done nothing at all!
I think the compiler has actually completely optimized out this statement. Which is a bit annoying,
when you are trying to explain things and the compiler is
too clever!
This one,
the one with the move(), that does call the move constructor.
So the cast to rvalue. For the assignment,
the lvalue calls the copy assignment operator, and the rvalue calls the move assignment operator.
Just one more thing
we need to know. When we have an inheritance hierarchy, and we are writing it move operators for a derived
class,
these need to call the corresponding versions in the base class.
So we have done this with the copy constructor.
We call the base class copy constructor with the same argument. For the move constructor,
we need to make sure that the base class move constructor gets called.
So we need to provide an rvalue.
The argument to the move constructor is actually an lvalue.
It has a name, and we could take its address.
So we need to call move(), to cast it to an rvalue.
And that will call the move constructor of the base class.
And finally, just to confirm, this does work with classes which were written before C++11.
A little anecdote, to finish off with. When Google was updating to C++11,
the first thing they did was they took their old code base and ran it through a C++ 11 compiler, just
to see what happened.
So they did not actually change anything.
And because the compiler was using move semantics with library objects, the performance was much faster.
And that actually reduced their electricity bill by a six figure, or possibly seven figure number,
in US dollars.
So there you are. Move semantics will save the planet!
Okay, so that is it for this video.
I will see you next time.
But until then, keep coding!