C++11/C++14 5. rvalue Reference and Move Semantics - 2020
"If you're an experienced C++ programmer and are anything like me, you initially
approached C++11 thinking, "Yes, yes, I get it. It's C++, only more so." But as you
learned more, you were surprised by the scope of the changes. auto declarations,
range-based for loops, lambda expressions, and rvalue references change the face of
C++, to say nothing of the new concurrency features. And then there are the
idiomatic changes. 0 and typedefs are out, nullptr and alias declarations are in.
Enums should now be scoped. Smart pointers are now preferable to built-in ones.
Moving objects is normally better than copying them.
- Effective Modern C++ by Scott Meyers
What's the implication of passing a variable by value?
When a function gets its parameter as by value, it does copy. Compiler knows how to copy it. If it's a user defined type, we may need to provide copy constructor, and probably assignment operator as well to be able to do a deep copy. However, copying is expensive. Actually, there are bunch of copying going on when we use STL containers. When an object tossed in by value, since it's a temporary object (rvalue), we're really wasting our precious resources including CPU as well as memory and so on.
So, we need to find a way to avoid such an unnecessary copy: move semantics
The primary purpose of introducing an rvalue is to implement move semantics which will be discussed later in this tutorial.
rvalue references enable us to distinguish an lvalue from an rvalue.
The C++11 Standard introduces rvalue references which bind only to rvalues. They are declared with two ampersands rather than one ampersand:
int&& rvalue_ref = 99;
Note that only an lvalue can be assigned to a normal reference, so if we do the following:
int& ref = 9;
we get an error: "invalid initialization of non-const reference of type int& from an rvalue of type int
We can only do:
int nine = 9; int& ref = nine;
Here is an example of rvalue reference. We have two functions: one that's taking an lvalue reference and the other one taking an rvalue:
#include <iostream> void f(int& i) { std::cout << "lvalue ref: " << i << "\n"; } void f(int&& i) { std::cout << "rvalue ref: " << i << "\n"; } int main() { int i = 77; f(i); // lvalue ref called f(99); // rvalue ref called f(std::move(i)); // rvalue ref called return 0; }
Note that the move(i) is calling a function taking rvalue. This is possible because move() takes an lvalue and converts it into an rvalue.
An expression is an rvalue if it results in a temporary object as shown in the example below.
#include <iostream> int getValue () { int ii = 10; return ii; } int main() { std::cout << getValue(); return 0; }
The getValue() is an rvalue.
Note that the value being returned is not a reference to ii, but it's just a temporary value.
In C++0x, we could use a normal or lvalue reference to bind temporary object, but only if it was const:
const int& val = getValue(); // OK int& val = getValue(); // NOT OK
In C++11, however, the rvalue reference lets us bind a mutable reference to an rvalue, but not an lvalue. In other words, rvalue references are perfect for detecting whether a value is a temporary object or not.
const int&& val = getValue(); // OK int&& val = getValue(); // OK
Let's compare both references:
void printReference (const int& value) { cout << value; } void printReference (int&& value) { cout << value; }
The printReference() function taking a const lvalue reference will accept any argument whether it be an lvalue or an rvalue. However, the second overloaded printReference*() taking only an rvalue reference.
In other words, by using the rvalue references, we can use function overloading to determine whether function parameters are lvalues or rvalues by having one overloaded function taking an lvalue reference and another taking an rvalue reference. In other words, C++11 introduces a new non-const reference type called an rvalue reference, identified by T&&. This refers to temporaries that are permitted to be modified after they are initialized, which is the cornerstone of move semantics.
#include <iostream> using namespace std; void printReference (int& value) { cout << "lvalue: value = " << value << endl; } void printReference (int&& value) { cout << "rvalue: value = " << value << endl; } int getValue () { int temp_ii = 99; return temp_ii; } int main() { int ii = 11; printReference(ii); printReference(getValue()); // printReference(99); return 0; }
With the output:
lvalue: value = 11 rvalue: value = 99
Note that we took out const from the void printReference (int& value) so that it can take only the lvalue.
Now, we have two distinct functions that can be overloaded: one taking only lvalue and the other one only taking rvalue. So, what?
It gives us a way to write more efficient program with less coding!
rvalue summary:
- int &&a;: new (C++11) declaration rvalue reference.
- non-const lvalue reference binds to an object.
- rvalue reference binds to a temporary object which typically will not be used again.
In C++03, there was a costly and unnecessary deep copies that can happen implicitly when objects are passed by value. We can avoid the performance hit due to deep copy by using a rvalue reference.
Now we have a way (overloading functions - taking lvalue or rvalue) to determine if a reference variable refers to a temporary object or to a permanent object. So, how it can be used?
The main usage of rvalue references is to create a move constructor and move assignment operator. A move constructor, like a copy constructor, takes an instance of an object as its argument and creates a new instance from original object. However, the move constructor can avoid memory reallocation because we know it has been provided a temporary object, so rather than copy the fields of the object, we will move them.
In other words, the rvalue references and move semantics allows us to avoid unnecessary copies when working with temporary objects. We do not want to copy the temporary which will go away. So, the resources needed for the temporary objects can be used for other objects.
The rvalues are typically temporary, and they can be modified. If we know that our function parameter is an rvalue, we can use it as a temporary storage, or take its contents without affecting the output of our program. This means that we can just move the contents rather than copy the contents of an rvalue parameter. This saves a lot of memory allocation and provides a lot of scope for optimization for large dynamic structures.
Here is the typical class definition using move semantics:
#include <iostream> #include <algorithm> class A { public: // Simple constructor that initializes the resource. explicit A(size_t length) : mLength(length), mData(new int[length]) { std::cout << "A(size_t). length = " << mLength << "." << std::endl; } // Destructor. ~A() { std::cout << "~A(). length = " << mLength << "."; if (mData != NULL) { std::cout << " Deleting resource."; delete[] mData; // Delete the resource. } std::cout << std::endl; } // Copy constructor. A(const A& other) : mLength(other.mLength), mData(new int[other.mLength]) { std::cout << "A(const A&). length = " << other.mLength << ". Copying resource." << std::endl; std::copy(other.mData, other.mData + mLength, mData); } // Copy assignment operator. A& operator=(const A& other) { std::cout << "operator=(const A&). length = " << other.mLength << ". Copying resource." << std::endl; if (this != &other;) { delete[] mData; // Free the existing resource. mLength = other.mLength; mData = new int[mLength]; std::copy(other.mData, other.mData + mLength, mData); } return *this; } // Move constructor. A(A&& other) : mData(NULL), mLength(0) { std::cout << "A(A&&). length = " << other.mLength << ". Moving resource.\n"; // Copy the data pointer and its length from the // source object. mData = other.mData; mLength = other.mLength; // Release the data pointer from the source object so that // the destructor does not free the memory multiple times. other.mData = NULL; other.mLength = 0; } // Move assignment operator. A& operator=(A&& other) { std::cout << "operator=(A&&). length = " << other.mLength << "." << std::endl; if (this != &other;) { // Free the existing resource. delete[] mData; // Copy the data pointer and its length from the // source object. mData = other.mData; mLength = other.mLength; // Release the data pointer from the source object so that // the destructor does not free the memory multiple times. other.mData = NULL; other.mLength = 0; } return *this; } // Retrieves the length of the data resource. size_t Length() const { return mLength; } private: size_t mLength; // The length of the resource. int* mData; // The resource. }; #include <vector> int main() { // Create a vector object and add a few elements to it. std::vector<A> v; v.push_back(A(25)); v.push_back(A(75)); // Insert a new element into the second position of the vector. v.insert(v.begin() + 1, A(50)); return 0; }
The example above adds two elements to a vector object and then inserts a new element between the two existing elements.
In its the simplest form, a move constructor looks like this:
A(A&& other) noexcept // C++11 - specifying non-exception throwing functions { mData = other.mData; // shallow copy or referential copy other.mData = nullptr; }
Note that it doesn't allocate any new resources and contents are moved not copied: what was in other moved to a new member, and what was in other disappeared. It pilfers other's resources and then sets other to its default-constructed state. The most critical fact is that there's no additional memory allocation. We just assign an address which only requires couple of machine instructions.
Suppose that address is pointing to an array which has millions of integer. We do not copy the elements. We do not create anything. We just move it. If we use old copy constructor for a class which has an 1-million element array member, we may have that many assignment operations, which is costly. Now, with the move constructor, we can save a lot.
// Move constructor. A(A&& other) : mData(NULL), mLength(0) { // Copy the data pointer and its length from the // source object. mData = other.mData; mLength = other.mLength; // Release the data pointer from the source object so that // the destructor does not free the memory multiple times. other.mData = NULL; other.mLength = 0; }
The move constructor is much faster than a copy constructor because it doesn't allocate memory nor does it copy memory blocks.
A move assignment operator has the following signature:
A& operator=(A&& other) noexcept { mData = other.mData; other.mData = nullptr; return *this; }
Note: non-cppying move assignment and rhs destroyed.
A move assignment operator is similar to a copy constructor except that before pilfering the source object, it releases any resources that its object may own. The move assignment operator takes the following steps:
- Release any resources that *this currently owns.
- Pilfer other's resource.
- Set other to a default state.
- Return *this.
Here's a definition for the move assignment operator:
// Move assignment operator. A& operator=(A&& other) { std::cout << "operator=(A&&). length = " << other.mLength << "." << std::endl; if (this != &other;) { // Free the existing resource. delete[] mData; // Copy the data pointer and its length from the // source object. mData = other.mData; mLength = other.mLength; // Release the data pointer from the source object so that // the destructor does not free the memory multiple times. other.mData = NULL; other.mLength = 0; } return *this; }
Since C++11 supports rvalue references, the Standard Library functions such as vector::push_back() now define two overloaded versions: one that takes const T& for lvalue arguments as before, and a new one that takes a parameter of type T&& for rvalue arguments. The code in the main() populates a vector with A objects using two push_back() calls:
std::vector<A> v; v.push_back(A(25)); v.push_back(A(75));
Both push_back() calls resolve as push_back(T&&) because their arguments are rvalues. The push_back(T&&) moves the resources from the argument into vector's internal A objects using A's move constructor. In C++03, the same program would generate copies of the argument since the copy constructor of A would be called instead.
The push_back(const T&) would have been called if the argument have had an lvalue:
#include <vector> int main() { std::vector<A> v; A aObj(25); // lvalue v.push_back(aObj); // push_back(const T&) }
However, we can enforce the selection of push_back(T&&) even in this case by casting an lvalue to an rvalue reference using static_cast:
// calls push_back(T&&) v.push_back(static_cast<A&&>(aObj));
Alternatively, we can use the new standard function std::move() to serve the same thing:
v.push_back(std::move(aObj)); //calls push_back(T&&)
It may seem that push_back(T&&) is always the best choice because it eliminates unnecessary copies. However, remember that push_back(T&&) empties its argument. If we want the argument to retain its state after a push_back() call, we should stick to copy semantics.
The following code shows how we can use move() to swap objects:
/* move.cpp */ #include <iostream> using namespace std; class A { public: // constructor explicit A(size_t length) : mLength(length), mData(new int[length]) {} // move constructor A(A&& other) { mData = other.mData; mLength = other.mLength; other.mData = nullptr; other.mLength = 0; } // move assignment A& operator=(A&& other) noexcept { mData = other.mData; mLength = other.mLength; other.mData = nullptr; other.mLength = 0; return *this; } size_t getLength() { return mLength; } void swap(A& other) { A temp = move(other); other = move(*this); *this = move(temp); } int* get_mData() { return mData; } private: int *mData; size_t mLength; }; int main() { A a(11), b(22); cout << a.getLength() << ' ' << b.getLength() << endl; cout << a.get_mData() << ' ' << b.get_mData() << endl; swap(a,b); cout << a.getLength() << ' ' << b.getLength() << endl; cout << a.get_mData() << ' ' << b.get_mData() << endl; return 0; }
Output:
$ g++ -std=c++11 -o move move.cpp $ ./move 11 22 0x1870010 0x1870050 22 11 0x1870050 0x1870010
C++11/C++14 Thread Tutorials
C++11 1. Creating Threads
C++11 2. Debugging with Visual Studio 2013
C++11 3. Threading with Lambda Function
C++11 4. Rvalue and Lvalue
C++11 5. Move semantics and Rvalue Reference
C++11 5B. Move semantics - more samples for move constructor
C++11 6. Thread with Move Semantics and Rvalue Reference
C++11 7. Threads - Sharing Memory and Mutex
C++11 8. Threads - Race Conditions
C++11 9. Threads - Deadlock
C++11 10. Threads - Condition Variables
C++11 11. Threads - unique futures (std::future<>) and shared futures (std::shared_future<>).
C++11 12. Threads - std::promise
C++11/C++14 New Features
initializer_list
Uniform initialization
Type Inference (auto) and Range-based for loop
The nullptr and strongly typed enumerations
Static assertions and Constructor delegation
override and final
default and delete specifier
constexpr and string literals
Lambda functions and expressions
std::array container
Rvalue and Lvalue (from C++11 Thread tutorial)
Move semantics and Rvalue Reference (from C++11 Thread tutorial)
Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization