
David A. answered 10/11/19
Experienced C++ tutor who loves to mentor and pass on my knowledge
The purpose of Move Semantics is to avoid costly and unnecessary deep copying, and in order to accomplish this state, C++ needs a way to differentiate between a lvalue and an rvalue (See my answer to "What does T&& (double ampersand) mean in C++11?").
To explain how Move Semantics works, let's create a sample vector class:
Let's take a closer look at the Move Constructor:
// Move constructor: used when copying to an rvalue
//
davidVector(davidVector &&rhs) noexcept { // move constructor
std::cout << "Calling move constructor" << '\n';
size = rhs.size;
ar = rhs.ar;
rhs.ar = nullptr;
}
Notice:
- The rvalue reference parameter (&&rhs). The move constructor will only bind to rvalues, not lvalues.
- A closer look in the body of the move constructor shows that we are 'stealing' the memory that the temporary object (rhs) is using and assigning it directly to the current (this) object.
- Once the resources have been stolen, we set the rhs ar pointer to nullptr so when delete is called on the temporary object, nothing would happen.
- We have effectively moved the memory of the temporary object to the current object (reused the resources), rather than allocating new memory and copying the temporary object's information (which is what the regular copy constructor does.
- With respect to davidVector &&rhs: The important point is not what rhs is, rather, what you know about the value to which rhs refers. Since rhs is an rvalue reference to a davidVector, you know for certain that whatever bound to it was an rvalue. This means it's bound to be destroyed when the full expression ends (and the end of the function), and you can safely treat it as an rvalue (by stealing its resources etc.).
Example calls to callByValue() and callByRef():
davidVector reusable = davidVector(1000);
callByRef(reusable); // Does NOT call Copy Constructor
callByValue(reusable); // Calls Copy Constructor
callByValue(davidVector(1000)); // Calls Copy and Move Constructor
callByValue(std::move(reusable)); // Calls Move Constructor
Bonus Topic: C++ compiler writers are very smart ;-)
Compilers writers are very smart and even though, as programmers, we are providing a way for the compiler to be more efficient with the move constructor, the compiler often has the smarts to see that temporary objects are being used and avoids the unnecessary copy operation and moves the resource anyway without the need of calling the move constructor. This is known as Copy Elision and almost all modern C++ compilers support it. In fact, in C++17, copy elision is guaranteed.
For learning purposes, we can disable the copy elision optimization so that we can see our move constructor operator "in action". We would, however, still implement move semantics since the compiler cannot always detect every condition where a copy can be elided.
To disable copy elision (for learning and debug purposes), you need to add a compiler flag:
For Xcode/g++, the compiler flag is: -fno-elide-constructors