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 ; //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. // 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. // Because x1 is a name and anything that has a name has an address. int& x3 = x1 ; //x3 is a lvalue. There is a name. // Right hand side x1 is also a lvale string str1 = "Te" ; string str2 = "st" ; //Left hand side is not a lvalue str1 + str2 = "Test" ; }
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 referecce !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.
Default Functions
If we don't define member functions such as constructor or the assignment operator in a class then the system defines the functions for us. The functions have "default" behavior and are called default functions. The "default" keyword introduced in C++ 11 states explicitly that the system provide the default implementation for the member function. We are talking about functions such as copy, move constructors, assignment operators. Usually the system will create the default implementation even if we don't say so but in certain cases it won't and we need to explicitly use the "default" keyword . For normal functions there is a bitwise copy of the original object. For the move member functions the system calls the move function on the data member variables and then copies them using the move operators if there are any.File: default1.cpp
#include <iostream> using namespace std ; class A { public: int x1 ; int x2 ; A( ) { cout << "A Empty constructor." << endl ; } }; class B { public: B( ) { cout << "B Empty constructor." << endl ; } B& operator=(B&& other) noexcept { cout << "B Move assignment operator for class B." << endl ; return *this ; } }; class C { public: B BObject1 ; C( ) { cout << "C Empty constructor." << endl ; } C& operator=(C&& obj1) = default ; }; int main() { A AObject1 ; AObject1.x1 = 10 ; AObject1.x2 = 20 ; A AObject2 ; AObject2 = AObject1 ; cout << AObject2.x1 << " " << AObject2.x2 << endl ; C CObject1 ; C CObject2 ; CObject2 = move(CObject1) ; return 0 ; } $ g++ default1.cpp ; ./a.exe A Empty constructor. A Empty constructor. 10 20 B Empty constructor. C Empty constructor. B Empty constructor. C Empty constructor. B Move assignment operator for class B. The statement AObject2 = AObject1 ; uses the default assignment operator which is a bitwise copy and if we print the contents of AObject2 we can see the values "10 20" that were in "AObject1" got copied over to "AObject2" : We then execute the statement: CObject2 = move(CObject1) ; The "move" changes the "CObject1" to a RValue. And in the class we are using the default function that the system generates for a move assignment operator. C& operator=(C&& obj1) = default ; Now the system will take "CObject1" and convert the data members to rvalues and then copy them over to "CObject2" . The class "C" has 1 data member which is "B BObject1 ;" . This is converted to a rvalue and then the class B's mmove assignment gets called.Note that the default move methods may not be created all the time. As an example if a move constructor is already defined for a class then the system expects the move assignment operator to be defined also and will not create it for us by default.
File: default2.cpp
#include <iostream> using namespace std ; class A { public: int x1 ; int x2 ; A( ) { cout << "A Empty constructor." << endl ; } //Move constructor A( A&& obj1 ) { cout << "A Empty constructor." << endl ; } }; int main() { A AObject1 ; AObject1.x1 = 10 ; AObject1.x2 = 20 ; A AObject2 ; AObject2 = move(AObject1) ; cout << AObject2.x1 << " " << AObject2.x2 << endl ; return 0 ; } $ g++ default2.cpp default2.cpp: In function ‘int main()’: default2.cpp:30:29: error: use of deleted function ‘constexpr A& A::operator=(const A&)’ 30 | AObject2 = move(AObject1) ; | ^ default2.cpp:6:7: note: ‘constexpr A& A::operator=(const A&)’ is implicitly declared as deleted because ‘A’ declares a move constructor or move assignment operator 6 | class A | ^ The above code shows that the system does not create a default move assignment operator implementation. If we don't need to use the move assignment operator then the code compiles fine. This can be illustrated by commenting out the code: AObject2 = move(AObject1) ; We can also tell the system to create the method explicitly using the default keyword.
File: default3.cpp
#include <iostream> using namespace std ; class A { public: int x1 ; int x2 ; A( ) { cout << "A Empty constructor." << endl ; } //Move constructor A( A&& obj1 ) { cout << "A Empty constructor." << endl ; } A& operator=(A&& other) = default ; }; int main() { A AObject1 ; AObject1.x1 = 10 ; AObject1.x2 = 20 ; A AObject2 ; AObject2 = move(AObject1) ; cout << AObject2.x1 << " " << AObject2.x2 << endl ; return 0 ; }
Perfect Forwarding
Let's say we have a scenario where we have a function that can take different arguments such as a reference, constant reference and rvalue reference. We want to call this function from another function that is more general and takes a template parameter. Our first attempt looks something like below.File: notfoward1.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 NotForwardToUseObject(const T& x) { UseObject(x); } template <typename T> void NotForwardToUseObject(T&& x) { cout << "NotForwardToUseObject:" << "Move reference." << endl ; UseObject(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; } $ g++ notforward1.cpp ; ./a.exe calling UseObject(Object &) calling UseObject(const Object &) calling UseObject(Object &&) ---- calling UseObject(Object &) calling UseObject(const Object &) NotForwardToUseObject:Move reference. calling UseObject(Object &) ---- We start by defining 3 versions of "NotForwardToUseObject" to take the 3 different parameters. templatePerfect forwarding allows us to write a cleaner version with fewer function calls as shown in the below program.void NotForwardToUseObject(T& x) template void NotForwardToUseObject(const T& x) template void NotForwardToUseObject(T&& x) The debug statement inside "void NotForwardToUseObject(T&& x)" shows that it is being called when we call "NotForwardToUseObject(move(object))" . However when we are inside "NotForwardToUseObject" the parameter has a name and when we call "UseObject" . cout << "NotForwardToUseObject:" << "Move reference." << endl ; UseObject(x); It becomes a lvalue and we end up calling the UseObject(Object &) instead of UseObject(Object&& x1) We could correct the issue by having another version of "UseObject" as shown in the next program.
File: notfoward2.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; } void UseObject1( Object&& x1 ) { cout << "calling UseObject1(Object &&)" << endl; } template <typename T> void NotForwardToUseObject( T& x1 ) { UseObject(x1); } template <typename T> void NotForwardToUseObject( const T& x1 ) { UseObject(x1); } template <typename T> void NotForwardToUseObject( T&& x1 ) { cout << "NotForwardToUseObject:" << "Move reference." << endl ; UseObject1( move(x1) ); } 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; }$ g++ notforward2.cpp ; ./a.exe calling UseObject(Object &) calling UseObject(const Object &) calling UseObject(Object &&) ---- calling UseObject(Object &) calling UseObject(const Object &) NotForwardToUseObject:Move reference. calling UseObject1(Object &&) ---- We created 1 more version "UseObject1" and are calling it from "NotForwardToUseObject" as: templatevoid NotForwardToUseObject( T&& x1 ) { cout << "NotForwardToUseObject:" << "Move reference." << endl ; UseObject1( move(x1) ); }
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.