Move Semantics
Move Semantics lets us utilize temporary objects. We need a way of intercepting a temporary object and "steal" it's resources. In order to do that we need to distinguish between a temporary resource and a normal resource.
RValue and LValues
If we can take the address of something then it is a lvalue else it is a rvalue. If it has a name then it is a lvalue. Usually things on the left side of an assignment operator are called lvalues and the ones on the right side are called rvalues.However this is not always the case. We can also have lvalues on the right hand side of a statement. The below program shows some example of this concept.File: values1.cpp
#include <iostream> using namespace std ; //https://stackoverflow.com/questions/15765893/what-is-the-reasoning-behind-the-naming-of-lvalue-and-rvalue //https://learn.microsoft.com/en-us/cpp/cpp/lvalues-and-rvalues-visual-cpp?view=msvc-170 //https://stackoverflow.com/questions/17357888/exact-difference-between-rvalue-and-lvalue template <typename T> constexpr bool is_lvalue(T&&) { return is_lvalue_reference<T>{}; } //Returns a rvalue int setValue() { return 6; } int global = 100; int& setGlobal() { return global; } int main() { int x1 = 100 ; //x1 is lvalue because it has an address //100 is a rvalue because it is temporary and does //not have a permanent address in memory. It may be stored //in a register but after the statement, //it's value is lost. cout << is_lvalue( x1 ) << endl ; cout << is_lvalue( 100 ) << endl ; // 100 = x1 ; //This produces a compiler error. //values1.cpp: In function ‘int main()’: //values1.cpp:20:6: error: lvalue required as left operand of assignment // 20 | 100 = x1 ; //The "100" does not have an address and is a rvalue and we cannot //put it to the left as it makes no sense. //The left of an assignment must be a lvalue . //setValue() = 100 ; //Compiler error as the function does not return a lvalue setGlobal() = 100 ; //The above is all right because the function // "setGlobal" returns a lvalue . int x2 = x1 ; //Here x2 is a lvalue and x1 is also a lvalue. int& x3 = x1 ; //x3 is a lvalue. There is a name. cout << is_lvalue( x3 ) << endl ; }
RValue Reference
We are familiar with lvalue references . We also have rvalue references. A lvalue reference is initialized with a lvalue variable. However a rvalue reference must be initialized with a rvalue. Every type can have a lvalue and a rvalue. Even a rvalue referemce !File: ref1.cpp
#include <iostream> using namespace std ; //This function determines if something //is lvalue or rvalue template <typename T> constexpr bool is_lvalue(T&&) { return std::is_lvalue_reference<T>{}; } //LVALUE REFERENCE void function1LValueRef( int& a1 ) { cout << "function1:" << a1 << endl ; } //RVALUE REFERENCE void function2RValueRef( int&& a1 ) { //Since a1 is a name it is a lvalue at this point inside the //function but the function can only take rvalues. cout << "Is a1 a lvalue: " << is_lvalue( a1 ) << endl ; //a1 = 25 ; } int main() { int x1 = 10 ; int y1 = 30 ; //Need to initialize references int&& x2R = 20 ; int&& x3R = 30 ; int& x3L = x1 ; //compiler error //A rvalue reference must be initialized //int&& x7 ; //Can only initialize it with a temporary object // int&& x7 = x1 ; //can reassign values to r reference //x2R is not pointing to x3R but rather it's //value is being updated. x2R = x3R ; cout << "x2R:" << x2R << endl ; x2R = 35 ; cout << "x2R:" << x2R << endl ; //Can initialize l-value reference with r-value //reference. Anything that has a name is l-value. int& x6R = x2R ; //cannot initialize lvalue //reference with a constant/rvalue //At the point of definition //int& x8 = 25 ; //can do //const int& x1 = 20 ; //Can only initialize rvalue reference with // rvalues //int&& x7 = x3L ; function1LValueRef( x1 ) ; function2RValueRef( 20 ) ; //Allowed // x2R = x1 ; x2R = 40 ; //reassignment to a r value reference cout << "x2R:" << x2R << endl ; //Not a reassignment of the lvalue reference. //changes the value of x1 and x3L "points" to x1 . x3L = y1 ; //x2R's value is being set to x1 x1 = x2R ; cout << "x1:" << x1 << endl ; //function 2 is r reference //Compiler error r-value reference expects a constant // function2RValueRef( x1 ) ; //ok move converts a l value to a r value function2RValueRef( move(x1) ) ; //x2R is a name so it's a l value at this point function1LValueRef( x2R ) ; cout << "Is x2R a lvalue: " << is_lvalue( x2R ) << endl ; cout << "Is 12 a lvalue: " << is_lvalue( 12 ) << endl ; function2RValueRef( move(x2R) ) ; cout << "x2R value: " << x2R << endl ; //compiler error even though x2R is rvalue !!! //But it has a name so now is lvalue !! // function2RValueRef( x2R ) ; function2RValueRef( 21 ) ; int x19 = 5 ; cout << x19 << " " << move(x19) << endl ; return 0 ; }
The key takeaway from the above code is that a rvalue reference is declared as: int&& x2R = 20 ; We can only assign a temporary value to it at this point. The "int&&" is a type. It's lvalue is "x2R" and it's rvalue from the above statement is "20" . Similarly in the function declaration: //RVALUE REFERENCE void function2RValueRef( int&& a1 ) { //Since a1 is a name it is a lvalue at this point inside the //function but the function can only take rvalues. cout << "Is a1 a lvalue: " << is_lvalue( a1 ) << endl ; //a1 = 25 ; } When we call this function we need to supply a rvalue. So we can call it like this: function2RValueRef( 20 ) ; However we cannot do: int&& x2R = 20 ; function2RValueRef( x2R ) ; Remember "x2R" is a name and therefore a lvalue and we cannot initialize a rvalue reference to a lvalue. This is the probably the most confusing part of rvalue references. The rvalue and lvalue are sort of properties of the object and we can change a variable from a lvalue to a rvalue with the std function "move" and by doing this we can call our function. function2RValueRef( move(x2R) ) ;
Usage
So we have the concept of rvalue references and we can assign temporary values to it but what's the advantage of this feature ? The below example shows how we can use rvalue references to take advantage of temporary objects when they are no longer needed.File: usage1.cpp
#include <iostream> using namespace std ; class Holder { public: Holder(int size) // Constructor { cout << "Argument constructor." << size << endl ; printf( "%p\n" , this) ; m_data = new int[size]; m_size = size; } Holder(const Holder& other) { cout << "Copy constructor." << m_size << endl ; m_data = new int[other.m_size]; // (1) std::copy(other.m_data, other.m_data + other.m_size, m_data); // (2) m_size = other.m_size; } Holder(Holder&& other) // <-- rvalue reference in input { cout << "Move copy constructor." << other.m_size << endl ; m_data = other.m_data; // (1) m_size = other.m_size; other.m_data = nullptr; // (2) other.m_size = 0; } Holder& operator=(const Holder& other) { cout << "assignment operator" << other.m_size << endl ; if(this == &other) return *this; // (1) delete[] m_data; // (2) m_data = new int[other.m_size]; std::copy(other.m_data, other.m_data + other.m_size, m_data); m_size = other.m_size; return *this; // (3) } Holder& operator=(Holder&& other) // <-- rvalue reference in input { cout << "Move assignment operator." << other.m_size << endl ; if (this == &other) return *this; delete[] m_data; // (1) m_data = other.m_data; // (2) m_size = other.m_size; other.m_data = nullptr; // (3) other.m_size = 0; return *this; } ~Holder() // Destructor { cout << "Destructor." << m_size << endl ; if ( m_data != NULL ) delete[] m_data; } private: int* m_data; size_t m_size; }; Holder createHolder(int size) { return Holder(size); } int main() { //Copy constructor Holder h1 = createHolder(1000) ; printf( "main %p\n" , &h1 ) ; cout << "Before Assignment.\n" << endl ; //Move Assignment operator h1 = createHolder(500); printf( "main %p\n" , &h1 ) ; cout << "End of main." << endl ; return 0 ; }
$ rm a.exe ; g++ -fno-elide-constructors usage1.cpp ; ./a.exe Argument constructor. 0x7ffffcc00 main 0x7ffffcc00 Before Assignment. Argument constructor. 0x7ffffcc10 Move assignment operator. Destructor.0 main 0x7ffffcc00 End of main. Destructor.500
We have a holder class with some additonal methods that take a rvalue reference as a parameter. One is a constructor and the other is the assignment operator. Holder(const Holder& other) { cout << "Copy constructor." << endl ; m_data = new int[other.m_size]; // (1) std::copy(other.m_data, other.m_data + other.m_size, m_data); // (2) m_size = other.m_size; } Holder(Holder&& other) // <-- rvalue reference in input { cout << "Move copy constructor." << endl ; m_data = other.m_data; // (1) m_size = other.m_size; other.m_data = nullptr; // (2) other.m_size = 0; } In the above snippet we see that with a regular copy constructor we allocate new memory and copy the values from the argument object. In the rvalue reference constructor we just use the argument's object pointer to memory and set the argument's object variables to null. The reason is so that the destructor does not destroy the "other" variables. We have "stolen" the resources of the temporary object to make our code more efficient. Holder createHolder(int size) { return Holder(size); } int main() { //Copy constructor Holder h1 = createHolder(1000) ; Output Argument constructor. 0x7ffffcc00 main 0x7ffffcc00 Before Assignment. What should happen in the above snippet, is that the "createHolder" returns a temporary object that gets assigned to "h1" forcing our rvalue reference constructor to be called. We see that the address of the object in Holder createHolder(int size) { return Holder(size); } is: 0x7ffffcc00 However after the "copy constructor" the address is the same: main 0x7ffffcc00 This is because of compiler optimization. The compiler does not destroy the initial created object and reuses it for "h1" so no "copy constructor" gets called. We can prevent optimization with the "-fno-elide-constructors" argument. $ rm a.exe ; g++ -fno-elide-constructors usage1.cpp ; ./a.exe Argument constructor.1000 0x7ffffcc00 main 0x7ffffcc00 Before Assignment. Argument constructor.500 0x7ffffcc10 Move assignment operator.500 Destructor.0 main 0x7ffffcc00 End of main. Destructor.500 That did not help as the compiler still did the optimization. However we notice that: h1 = createHolder(500); did cause our move assignment operator to get called. Move assignment operator.500 Destructor.0 Once it was done with the assignment, the compiler decided to destroy the object returned by: createHolder(500) Whose "m_size" is now zero as it was reset by the rvalue reference assignment operator.
File: usage2.cpp
#include <iostream> using namespace std ; class Holder { public: Holder(int size) // Constructor { cout << "Argument constructor." << endl ; m_data = new int[size]; m_size = size; } Holder(const Holder& other) { cout << "Copy constructor." << endl ; m_data = new int[other.m_size]; // (1) std::copy(other.m_data, other.m_data + other.m_size, m_data); // (2) m_size = other.m_size; } Holder(Holder&& other) // <-- rvalue reference in input { cout << "Move copy constructor." << endl ; m_data = other.m_data; // (1) m_size = other.m_size; other.m_data = nullptr; // (2) other.m_size = 0; } Holder& operator=(const Holder& other) { cout << "assignment operator" << endl ; if(this == &other) return *this; // (1) delete[] m_data; // (2) m_data = new int[other.m_size]; std::copy(other.m_data, other.m_data + other.m_size, m_data); m_size = other.m_size; return *this; // (3) } Holder& operator=(Holder&& other) // <-- rvalue reference in input { cout << "Move assignment operator." << endl ; if (this == &other) return *this; delete[] m_data; // (1) m_data = other.m_data; // (2) m_size = other.m_size; other.m_data = nullptr; // (3) other.m_size = 0; return *this; } ~Holder() // Destructor { cout << "Destructor." << endl ; if ( m_data != NULL ) delete[] m_data; } private: int* m_data; size_t m_size; }; Holder createHolder(int size) { return Holder(size); } int main() { // Holder h1(1000) ; { Holder h2(1000) ; cout << "Before Assignment.\n" << endl ; //Assignment operator h1 = move( h2 ) ; } return 0 ; }
$ g++ -fno-elide-constructors usage2.cpp ; ./a.exe Argument constructor. Argument constructor. Before Assignment. Move assignment operator. Destructor. Destructor.
The above file shows another way we can use rvalue references. Holder h1(1000) ; { Holder h2(1000) ; cout << "Before Assignment.\n" << endl ; //Assignment operator h1 = move( h2 ) ; } We know that "h2" is going out of scope and we want to use it's resources but it is not a rvalue. However with "move" we change the property to rvalue and "steal" it's resources.
Perfect Forwarding
File: foward1.cpp
#include <iostream> using namespace std ; class Object { public: Object() = default; void SetName(const string &name) { name_ = move(name); } string GetName() const { return name_; } private: string name_; }; void UseObject(Object& x1) { cout << "calling UseObject(Object &)" << endl; } void UseObject(const Object& x1) { cout << "calling UseObject(const Object &)" << endl; } void UseObject(Object&& x1) { cout << "calling UseObject(Object &&)" << endl; } template <typename T> void NotForwardToUseObject(T x) { UseObject(x); } template <typename T> void ForwardToUseObject(T &&x) { UseObject(static_cast<T &&>(x)); } template <typename T> void PerfectForwardToUseObject(T &&x) { UseObject(forward<T>(x)); } int main() { Object object; const Object const_object; UseObject(object); UseObject(const_object); UseObject(move(object)); cout << "----" << endl; NotForwardToUseObject(object); NotForwardToUseObject(const_object); NotForwardToUseObject(move(object)); cout << "----" << endl; ForwardToUseObject(object); ForwardToUseObject(const_object); ForwardToUseObject(move(object)); cout << "----" << endl; PerfectForwardToUseObject(object); PerfectForwardToUseObject(const_object); PerfectForwardToUseObject(move(object)); } $ rm a.exe ; g++ forward1.cpp ; ./a.exe calling UseObject(Object &) calling UseObject(const Object &) calling UseObject(Object &&) ---- calling UseObject(Object &) calling UseObject(Object &) calling UseObject(Object &) ---- calling UseObject(Object &) calling UseObject(const Object &) calling UseObject(Object &&) ---- calling UseObject(Object &) calling UseObject(const Object &) calling UseObject(Object &&)
There are certain cases where we call a function that can take a lvalue or rvalue reference.In the above file we can see such a function: void UseObject(Object& x1) { cout << "calling UseObject(Object &)" << endl; } void UseObject(const Object& x1) { cout << "calling UseObject(const Object &)" << endl; } void UseObject(Object&& x1) { cout << "calling UseObject(Object &&)" << endl; } templatevoid NotForwardToUseObject(T x) { UseObject(x); } We want that the right "UseObject" should be called when we call a common function such as "NotForwardToUseObject" . We want the "UseObject" to be called for lvalue or rvalue references. However we know that as long as we have a name it is a lvalue. To preserve the concept of lvalue and rvalue we have the concept of perfect forwarding. template void PerfectForwardToUseObject(T&& x) { UseObject(forward (x)); } The syntax is not very intuitive or logical but we define a template function and the argument as "T&& x". Now with the "forward" function we can preserve the lvalue or rvalue of the argument.