C++11 brought us a whole host of useful features, and expansions on STL. One such feature is the introduction of “move semantics” to the C++ language. Put simply, move operations are the ability to “move” a resource (memory, file handle, etc.) instead of copying it. Imagine you are back in college, and missed an important class. To avoid falling behind, you ask a friend if you can copy their notes. As it turns out, your friend took the course last semester and no longer has a need for them. Instead of going through the trouble of copying them, he simply agrees to give you the original notes. In effect, this is what you can achieve with move operations. However, there is an obvious problem here. You can only take the original notes from your friend if he no longer has need for them. Doing otherwise would be incredibly rude.
Back in the programming realm, how do we know if we are able to move a resource, as opposed to copying it? To better answer this question, it will be helpful to first understand the difference between L-values, and R-values.
R-Values vs L-Values
L-values are pieces of the equality statement which exist after the statement is evaluated.
R-values are pieces of the equality statement which do not exist after the statement is evaluated. In effect, R-values are temporary, L-values are not.
From what I’ve heard, the initially referred to “right-hand” value, and “left-hand” value. This indicated whether the value was on the left or right side of the equality statement. So, according to this, “x = 10;”, x would be our L-value, and 10 our R-value. While this definition no longer applies in modern C++, it can still serve as a mnemonic device to help you remember the difference. R-values are temporary to the statement they reside, so you would never see an R-value on the left side of an equality statement.
To answer my earlier question, we know we can move a resource as opposed to copy it if the resource comes from an R-value. After all, R-values are temporary. If we attempt to copy a temporary resource, then we may as well just move ownership instead.
I find the best way to learn is by re-implementing common STL functionality. For this example, we will re-implement an STL vector style collection class.
template <class T>
Collection(const std::initializer_list<T>& rInitializer);
Collection(const Collection& rCollection);
Collection& operator=(const Collection& rCollection);
// Move overloads
Collection& operator=(Collection&& rCollection);
T& operator(int32_t index);
const T& operator(int32_t index) const;
size_t size() const;
There we go, now we have the basics of a collection class. Now you might notice some interesting syntax here. Specifically:
This is what’s known as an “R-value reference”. In C++ it is how we can define a parameter overload as being an R-value vs an L-value. Using this syntax we can define our “move constructor” as well as our “move assignment operator”.
Move Constructor Implementation
Similarly to a copy constructor/assignment operator, our move constructor/assignment operator will have nearly identical implementation. They will both have two important pieces.
- First we want to transfer the resource from the R-value to the L-value.
- Second we want to invalidate the resource stored on the R-value. This is an important step, as we want to prevent the R-value from destructing the contained resource.
template <class T>
mSize = rCollection.mSize;
T* pData = rCollection.mpData.release();
And that’s it! the call to ‘release()’ on the smart pointer sets the pointer to null and relinquishes ownership. The move assignment operator is nearly identical, with the exception of the possible need to destruct any existing resources on the L-value. An interesting note, contrary to a copy assignment operator it is not necessary with the move assignment operator to check the parameter’s pointer against the current object’s pointer. This makes sense after all. It’s difficult to think of reasonable situation in which you could set an R-value equal to itself.
Why is this Important?
Move semantics are important because they allow you to entirely avoid superfluous resource acquisition/release. This means avoiding memory allocation, file access, mutex acquistion, reference counting, etc. It may not seem significant. After all, the compiler is pretty good when it comes to avoiding these situations if it can. That said, you should never rely on the compiler to do the smart thing. With just a little extra effort you can ensure that the compiler does the right thing in all situations involving copying temporaries.
I hope this has helped some of you, and if you have any questions please post them in the comments.