Move semantics are another game-changer introduced in C++11. One large complaint about writing C++ code was that copy construction and copy assignment can cause performance issues because spurious copies may be expensive. This was somewhat alleviated by the little-understood notion of copy elision (where a copy operation can be skipped in some special circumstances where the compiler can guarantee the operation behaves consistently with or without the copy), but was still a large problem in practice. Consider:
#include <string>
#include <vector>
std::vector<std::string> moby_dick_sentences() {
std::vector<std::string> sentences;
// Read in Moby Dick, split into sentences.
return sentences;
}
int main() {
std::vector<std::string> sentences;
sentences = moby_dick_sentences();
}
In C++98, this code would have poor performance because of the number of times we must copy each sentence from Moby Dick into a new area of memory. The vector copy constructor will be called once for the assignment to “sentences” within main()
from the hidden return value from the function call. The copy operation is elided for the assignment of the local
variable “sentences” to the actual, hidden return value in moby_dick_sentences()
.
However, the astute observer will notice that the hidden return value will be immediately destroyed after the copy operation took place.
This is where the power of move semantics comes in: there are many situations in which we perform a copy operation followed almost immediately by destroying the source of the copy, especially when temporaries are involved — rather than perform a copy and then destroy, why not just STEAL the resources away from the object that’s about to be destroyed? That’s move semantics in a nutshell: rather than perform a potentially expensive copy operation, just take the resources away from the object that’s about to be destroyed (but leave it in a valid state as it’s still going to be destroyed!).
So how does it work? It works through a corollary feature called “rvalue references”. I don’t want to spend a ton of time getting into value categories and the theory behind them, but an easy way to think about them is: when you want an lvalue reference (a thing with a name and a memory location), you use the &
token, and when you want an rvalue reference (a thing with a memory location but without a name), you use the &&
token. There are two new special member functions where you will see this new token used most often: constructors and assignment operators. Just like there’s a copy constructor, there’s now a move constructor as well. Similarly, just like there’s a copy assignment operator, there’s now a move assignment operator as well.
Talking about how to properly use move semantics and rvalue references is a lecture in and of itself, so I won’t go too far into how you write your own, but the basic gist is:
struct S {
S(); // Default constructor
~S(); // Destructor
S(const S&); // Copy constructor
S(S&&); // Move constructor
S& operator=(const S&); // Copy assignment operator
S& operator=(S&&); // Move assignment operator
};
You’ll notice that the copy operations accept a const lvalue reference while the move operations accept a non-const rvalue reference. That’s because the moved-from object (the source) needs to be left in a valid state when the operation completes. A typical implementation may look something like:
#include <utility>
S::S(S &&other) {
my_pointer_member = other.my_pointer_member;
my_length_member = other.my_length_member;
other.my_pointer_member = nullptr;
other.my_length_member = 0;
}
S &S::operator=(S &&other) {
using std::swap;
swap(my_pointer_member, other.my_pointer_member);
swap(my_length_member, other.my_length_member);
return *this;
}
This demonstrates how you can steal the internal state from the source object, and then leave the source object with something valid to destroy when its destructor is called. This prevents the rvalue object from freeing a pointer we just stole out from under it (aka, double free bugs).
One neat thing about move semantics is: you don’t have to know it exists and still benefit! The entire STL has been modified to add move semantics to various container datatypes (std::string, std::vector
, etc), so your code automatically benefits without you having to change it. My initial example with sentences from Moby Dick no longer calls any copy operations because the compiler automatically selects the move assignment operator when compiling in C++11 or later. However, due to the performance improvements move operations provide, class authors should think about move semantics any time they find themselves writing a copy constructor or copy assignment operator to decide whether move operations will benefit their class. However, it’s not required because all common move operations will gracefully degenerate into a copy operation if no move operator is provided.
In part 3, I’ll discuss automatic type inference.
Like what you read? Download our white paper “Advanced Static Analysis for C++” to learn more.
{{cta(’42fd5967-604d-43ad-9272-63d7c9d3b48f’)}}