Move Semantics in Practice
In this article, I’m going to talk about some practical uses, practical motivations, and common misunderstanding for move semantics. I’ve seen a lot of posts talk about move semantics in a very contrived way, and I’m here to make things more practical. If you’re a beginner to move semantics check this post out and come back. Also, a lot of these code snippets are similar so don’t be intimidated!
Vector::resize calls move
Our first example is going to look at vectors. Here’s a quick string and vector implementation.
I want to draw your attention to vector::resize
. This is when the vector allocates double the capacity worth of memory on the heap and copies over the data to its new location. Line 50 triggers the string assignment operator which triggers the copy constructor since I’ve used the copy and swap idiom. Whenever a vector gets resized, all string objects will get deep copied, via the string construction, to their new location and the old one gets destroyed. But all that we really needed to move was string object, not the memory on the heap that string::data
points to. We want to do a shallow copy of the string object because the old string object is getting destroyed anyways. This is where the move constructor comes to the rescue.
The perfect use case for move is when your current object needs to be copied somewhere else and the current object (often temporary) is no longer needed. And this is exactly what vector::resize
is trying to do. We’re going to write a move constructor that does a moves the external resources of string and clears out the old one’s pointer, so we don’t run into any trouble when the old one is destroyed.
We’ve added a move constructor to the string object on lines 29–33. We set the data
to the rvalue’s (that
) data
and we also have to clear out the rvalue’s data
or else the string destructor would delete that.
On line 56, we now use std::move(data[i])
to invoke the move assignment which defaults to the regular assignment operator. But this is actually perfectly fine because the move constructor is invoked when constructing that
from data[i]
when creating the assignment operator’s parameters. Thus, line 56 is now moving all the strings to their new location and transferring over rather than copying their external resources. This has the potential to save a lot of time for vector::resize
.
Creating something new for something else
If you ever allocate memory just to have it copied later, you should use move semantics instead. Let’s look at the string operator+
.
In the operator+
, we allocate enough memory for both this
and that
strings, then memcpy
them in. Then on line 26, we call the string constructor which deep copies all the data from new_data
into itself. Did we really need to deep copy new_data
? new_data
was created just to pass into the string constructor, and we have no more use for it. It would be best to just move the external resources of new_data
to the new string.
On lines 14–18 we’ve made this new constructor that takes an rvalue reference to a char*. This is very similar to a move constructor, but it’s not. A move constructor for class string would take an rvalue reference to the string type. We’re just trying to transfer the resources of a char* over to the string.
On line 31, we now invoke the constructor that takes in char*&& p (an rvalue reference for a char*) by calling std::move
on new_data
. This constructor knows it’s taking in a temporary (rvalue) and should move the external resources from p
to string::data
, not deep copy it. By having the move constructor take in a temporary, it allows the program to move the resources from p
to string::data
with confidence because it knows p
no longer needs them.
The take away here is if you create something with external resources just to be copied, you should probably use move semantics.
What your mother never told you
A lot of tutorials introduce move semantics as a way to better deal with temporaries. So for example,
string myString(function_that_returns_a_string());
you would think that the returned string is a temporary and that myString
would invoke the move constructor. But this is partly false. Yes, function_that_returns_a_string()
is an rvalue, but the compiler does something even better that avoids move semantics in these cases.The compiler does copy elision.
Copy elision is a compiler optimization where instead of creating temporaries, temporary objects are just constructed in the targets memory, thus never needing to invoke a copy/move constructor. So instead of function_that_returns_a_string
creating a temporary, the returned object is literally constructed in myString
’s memory, thus there’s no need to copy/move it. This is mandated for C++17 compilers and optional for C++11 compilers.
Copy elision is even better than move semantics though, so don’t feel too bad. Moving an object means constructing an object and then moving its resources over, and copy elision optimizes things even more by just creating temporary objects in the place they’ll end up in.
When should you define a move constructor?
I really like this definition of move semantics from this SO post
Move semantics allows an object to take ownership of some other object’s external resources.
So the answer is, whenever your class handles external resources, it’s a good idea to write a move constructor.