So, in a previous post we covered the copy constructor and copy assignment operator. We also had a look at copy elision. Now it’s time to talk about the move constructor and move assignment operator. To explain what these special move methods are and why we need them, we are going to have another quick look at our Bond
example. As before we are including a subclass to prevent elision.
#include <iostream>
class Bond
{
private:
double Rate;
public:
Bond(double rate) : Rate(rate)
{
std::cout << "Constructing" << std::endl;
}
// copy assignment
Bond& operator=(const Bond& other)
{
std::cout << "Copy assigning" << std::endl;
Rate = other.Rate;
return *this;
}
// copy constructor
Bond(const Bond& other) : Rate(other.Rate)
{
std::cout << "Copy constructing" << std::endl;
}
};
class NamedBond : public Bond
{
private:
string Name;
public:
NamedBond(string name, double rate) : Bond(bond), Name(name)
{
std::cout << "Sub class constructing" << std::endl;
}
}
When we run the following code:
Bond b(new NamedBond(1.2, "Name"));
we will see something like:
Constructing
Sub class construction
Copy constructing
What happens is, we build an object of type NamedBond
and then copy it into a Bond
object. But we don’t actually use the NamedBond
object we constructed. It just exists as a temporary object without a name. We don’t actually have to copy it into the Bond
object b
. We can move it.
So, we are going to add two more special methods, the move constructor and the move assignment operator. This is how we define them:
// move constructor
Bond(Bond&& other) : Rate(other.Rate)
{
std::cout << "Move constructing" << std::endl;
}
// move assignment
Bond& operator=(Bond&& other)
{
std::cout << "Move assigning" << std::endl;
Rate = other.Rate;
return *this;
}
Now when we run:
Bond b(new NamedBond(1.2, "Name"));
we will see:
Constructing
Sub class constructing
Move constructing
So, the move constructor was used here. Why did the compiler decide to do that? Well, the compiler knows we are just using a temporary value to create b
. Also the &&
modifier on the move constructor’s parameter, indicates it accepts a temporary value. This means that the move constructor will always get called when initialising with a temporary object. For example, suppose we have a function such as:
static NamedBond NamedBondFactory(double rate, string name)
{
return new NamedBond(rate, name);
}
The following code:
Bond b(NamedBondFactory(1.2, "Name"));
will also go through the move constructor rather than the copy constructor. Similarly, if we have defined an addition operator on NamedBonds, then
NamedBond a(1.2, "Name1");
NamedBond b(2.4, "Name2");
NamedBond c(a + b);
will use the move constructor rather than the copy constructor.
We see the same thing with move assignment. However, just as with copy assignment, the compiler is much stricter and will not elide move assignments. So we can see the effect without using the NamedBond
subclass.
So, suppose we run the following code:
Bond b(1.2);
b = Bond(2.3);
We will see:
Constructing
Construction
Move Assigning
We did not pass through the copy constructor. This is because the object created by Bond(2.3)
is only a temporary object. Similarly, suppose we have a function which returns a Bond
object, let’s call it BondFactory
. The following code:
Bond b(1.2, "Name");
b = BondFactory();
Will print:
Constructing
Constructing
Move assigning
Why would we bother with these two extra methods? Why not just use the copy constructor and copy assignment? Well, the secret is in the signature. Firstly, we know that the move methods only take arguments that are temporary values. Secondly, the parameters to the move constructor and move assignment are not const
. Taken together this means that, rather than copy data/resources the source object, we can just move data/resources out of it.
We can say that the move methods allow us to transfer ownership of resources from one object to another. For a class like Bond
this distinction is a little pointless. But for more complex objects it can be crucial. For example, suppose we have an object that contained an array, our copy constructor and copy assignment would copy all the values in the array, whereas the move constructor and move assignment would just copy the pointer and set the pointer in the source to null. This pattern, a deep copy in the copy methods and a shallow copy in the move methods, is fairly typical. Also, when we move, we we always make sure to not leave any reference in the source object to whatever resources we are moving.
We can also invoke the move methods explicitly. We do this by using the std::move()
function. So, as we know, the following code,
DerivedBond b(1.2, "Name");
Bond c(b);
will use the copy constructor to initialise c
. However, if we use std::move
like so:
DerivedBond b(1.2, "Name");
Bond(std::move(b));
the move constructor will be used. This is especially useful when we want to transfer ownership of a resource that is held by an object that is not temporary. We can also use std::move
to invoke the move assignment operator. Whereas
Bond a(1.3);
DerivedBond b(1.2, "Name");
a = b;
will invoke the copy assignment method, this code:
Bond a(1.3);
DerivedBond b(1.2, "Name");
a = std::move(b);
will invoke move assignment.
The examples we have covered are a little bit complicated. Usually, when they are explained the effect of copy elision is ignored, which I think it quite confusing. If you try to run some examples you see on the internet they will not work at all the way they should, because the moves/copies are elided by the constructor. Our next post will bring everything we have covered together in a single example.