When should we write our own move constructor and move assignment operator?
Introduction – Why Moving Resources
When writing a program you will encounter a case where you need to move (large) resources around from one object to another.
In C++ we have Move Semantics which is a way to move resources to avoid making copies in the memory which not only will make our program slower but also use more space than necessary.
We are not discussing the Move Semantics here, because there are a lot of resources you can find on the internet explaining rvalues and Move Semantics.
What is not so obvious is that when we can rely on the compiler to help us move our resources around, and when we have to write our own move constructor and move assignment operator.
We will see in the sections below, with some examples. Of course, it is required that you use Modern c++, at least c++11.
Implicitly Declared and Defined Special Member Functions
If we create a simple struct like the one below, the compiler will implicitly generate some special functions for us so that we don’t have to write verbose code. We will have the following functions generated by the compiler:
- Default constructor
- Copy ctor
- Move constructor
- Copy assignment operator
- Move assignment operator
- Destructor
Knowing this is important for us to understand whether or not we need to write them when we manage (large) resources.
Moving Resources
We can manage resources in a variety of ways, the most common way is using std::vector, but in other cases, we may want to use raw or smart pointers.
We may also need to manage OSes’ resources when creating a wrapper for example like a socket handle in Linux.
Managing Resources with std::vector
When writing a class that manages vectors, we don’t have to specifically write move constructor and move assignment operator because std::vector has implemented it for us. Look at the following example:
In our Data class, we only implement a default constructor and that’s it. But as you see we can copy and move our resources.
Data data2 = data;
The line above invokes a copy constructor, and the following line invokes the move constructor.
Data data3 = std::move(data);
If we see the output of the program, we’ll see something like:
Data's internalData is at: 0x558d72e74eb0
Data2's internalData is at: 0x558d72e75460
Data3's internalData is at: 0x558d72e74eb0
data is now empty
We can see that data2 has a different address that is because the resources are copied into a new space in the memory whereas data3 has the same address as data because the resources are just moved. As a result, data becomes empty because its resources have been released from it.
What about Smart Pointers?
There are shared pointer and unique pointer, but we will focus on shared pointer here because a unique pointer doesn’t allow you to copy it, well because it has to be unique :), it can only be moved.
This program’s output is:
Data's internalData is at: 0x5599c3db8ec0
Number of owners: 1
Data2's internalData is at: 0x5599c3db8ec0
Number of owners: 2
Data3's internalData is at: 0x5599c3db8ec0
Number of owners: 2
data is now null
In this case, our addresses are all the same, it is because _shared_ptr_ is for sharing resources, so when you make a copy by calling this line:
Data data2 = data;
The resources are not copied but they are now shared, this can be seen from the owners’ count that becomes 2 after that line.
Now if we call the following line:
Data data3 = std::move(data);
the move constructor is invoked, the data3’s internalData points to the same address as data2’s internalData but now data has no access to the resources anymore because they have been transferred to data3.
In this scenario, too, we can rely on the compiler to do its job to implement the move constructor (and all other special member functions) for us.
What about raw pointer?
In some scenarios we may want to manage raw pointer, let’s see one example.
Just like the previous example, we try to rely on the compiler to do its job for us. The output of this program is as follows:
Data's internalData is at: 0x5565b0edaeb0
Data2's internalData is at: 0x5565b0edaeb0
Data3's internalData is at: 0x5565b0edaeb0
All of them are pointing to the same address, something is not right here. At least we expect Data2’s internalData to point to a different address because it is supposed to copy.
This is clearly not working. The reason is that the implicitly generated copy constructor does a member-wise copy of the members, so the address gets copied, not the data.
Another important thing that is missing from the code is that we don’t release the memory when the object is destroyed this will cause a memory leak. So we need to write our own destructor.
Now what happens after we added the destructor it that this program will crash when we execute it.
Data's internalData is at: 0x5632d3066eb0
Data2's internalData is at: 0x5632d3066eb0
Data3's internalData is at: 0x5632d3066eb0
double free or corruption (!prev)
Aborted (core dumped)
This is because we haven’t implemented our special member functions below properly:
- Copy constructor
- Move constructor
- Copy assignment operator
- Move assignment operator
Let us now write the full implementation as follows:
The output will be correct as shown below:
Data's internalData is at: 0x5638e02c2eb0
Data2's internalData is at: 0x5638e02c4270
Data3's internalData is at: 0x5638e02c2eb0
Data is now empty
Data2‘s internalData now points to a different address and has its own copy of the data, and Data becomes empty after its internalData is moved to Data3.
Conclusion
After experimenting with the different scenarios above we can now conclude that we only need to write our own move constructor and move assignment operator when our class manages raw resources such as raw pointers and OSes’ handles such as sockets. Otherwise, we can rely on the compiler to generate them for us.
Rule of Three/Five
One important thing to remember is the Rule of Three which says:
If you need to explicitly declare either the destructor, copy constructor, or copy assignment operator yourself, you probably need to explicitly declare all three of them.
For Modern C++ we need to add two more which are move constructor and move assignment operator hence the Rule of Five.
In our example above, we needed to write our destructor, so we needed all five special member functions.