Understanding Rvalue References & Move Semantics of C++ 11

Discussion in 'C++' started by BiplabKamal, Feb 5, 2016.

  1. C++ always has been creating fast programs, still there were loop holes which could substantially slow down C++ programs until C++11. This was due to temporary object creation and expensive object copies. C++11 added a new reference type called 'rvalue reference' and a new concept of moving object instead of copying, also known as Move Semantics. Let us first dig into the problem we are talking about which is solved by Rvalue Reference and Move Semantics. Look into the following code carefully:

    Code:
    #include<vector>
    #include<iostream>
    using namespace std;
    class MyClass
    {
    private:
        static int instancecount;
        int* pIntArray;
        int Size;
    public:
        MyClass(int size)
        {
            pIntArray = new int[size]; // Allocating memory
            for (int i=0; i < size;i++)
                pIntArray[i] = i;
            Size = size;
            cout << "New instance of MyClass is created" << endl;
            cout << "Total Number of instances: " << ++instancecount << endl<<endl;
        }
        MyClass(const MyClass& obj)// Copy constructor
        {
            Size = obj.Size;
            pIntArray = new int[Size]; // Allocating new memory
            for (int i=0; i < Size;i++)
                pIntArray[i] = obj.pIntArray[i];
            cout << "New instance of MyClass is created with the copy of another instance" << endl;
            cout << "Total Number of instances: " << ++instancecount << endl << endl;
        }
        
        MyClass& operator=(const MyClass& source) // Copy assignment operator
        {
            if (this != &source)// Checking if same object is assigned
            {
                delete pIntArray; // releasing old memory because size of new memory might be different
                Size = source.Size;
                pIntArray = new int[Size]; // Allocating new memory
                for (int i = 0; i < Size;i++)
                    pIntArray[i] = source.pIntArray[i];
                cout << "Existing instance of MyClass is being overwritten with the copy assignment" << endl;
                
            }
            return *this;
        }
        ~MyClass()
        {
            cout << "One instance of MyClass is destroyed" << endl;
            cout << "Total Number of instances: " << --instancecount << endl<<endl;
        }
        
    };
    int MyClass::instancecount = 0;
    
    int main()
    {
        vector<MyClass> V1;
        /* Constructor and Copy Constructor both are called. Two MyClass object is created, 1st a object is created and then a copy is created to pass the argument by value. Note the first instance is a temporary object, and will be destroyed immediately.*/
        V1.push_back(MyClass(1)); 
        MyClass obj1(10); // Constructor is called
        obj1 = MyClass(20); /* Copy assignment operator is called passing a temporary object, which will be destroyed immediately*/
        
        return 0;
    }// Elements of the vector V1 and obj1 will be destroyed here
    
    Code:
    New instance of MyClass is created
    Total Number of instances: 1
    New instance of MyClass is created with the copy of another instance
    Total Number of instances: 2
    One instance of MyClass is destroyed
    Total Number of instances: 1
    New instance of MyClass is created
    Total Number of instances: 2
    New instance of MyClass is created
    Total Number of instances: 3
    Existing instance of MyClass is being overwritten with the copy assignment
    One instance of MyClass is destroyed
    Total Number of instances: 2
    One instance of MyClass is destroyed
    Total Number of instances: 1
    One instance of MyClass is destroyed
    Total Number of instances: 0
    
    If you look into the above code, in main() function you will find one statement ( V1.push_back(MyClass(1)); ) invoking copy constructor and the other statement( obj1 = MyClass(20); ) invoking copy assignment operator. In both of the cases a temporary object has been created on the fly which is a candidate for immediate destruction.

    Now if you look into the class structure and the definition of copy constructor and copy assignment operator of MyClass both are allocating new memory and copying the content of the memory from the source instance(they are supposed to do that), and those source objects are getting destroyed immediately and releasing their own memory. So here is a redundant memory allocation and de-allocation. What if we could reuse the memory allocated by the source object(temporary object)? Note that in case of non-temporary object the functioning of copy the constructor and the copy assignment operator is fine because we need to have separate memory to avoid dangling pointer problem. But in the above code we know the source objects are temporary(rvalue) and going to be destroyed immediately. Consider pushing a large number of objects in the vector and the burden of copy constructor. C++11 introduced the concept of move semantics to overcome the limitation. Move semantics talks about moving objects instead of copying object.

    To get the move semantics work C++11 introduced a new kind of reference type called rvalue reference. To understand rvalue reference we need to have clear idea about the differences between the concept of lvalue and rvalue objects ( or expressions) and referencing to them. Prior to C++11 we could create reference type only to bind with lvalue object. But C++11 allows you to bind a reference variable to a rvalue object or expression. Let us first look into lvalue and rvalues.

    Lvalue and Rvalue



    Every expression in C++ is an lvalue or an rvalue. For example:
    Code:
        int i;// i is an lvalue
        int j = 5;// j is an lvalue and 5 is an rvalue
        i = 10; /* Left side expression of the assignment operator is a modifiable lvalue expression and right side             expression is an rvalue expression*/
        //i+7 = 10; // Error: left operation must be a modifiable lvalue
        (i < j) ? i : j = j; // Valid because the left hand expression results to a modifiable lvalue
        //int* p = &10;// The statement is invalid because you cannot retrieve the address of an lvalue
    
    int MyFn(); //returns an rvalue but 
    int& MyFn(); //returns an lvalue.
    
    Following code is valid -
    Code:
        int& MyFn(); // returns lvalue
        MyFn() = 200; // OK,Left side is an lvalue
        int *p = &MyFn(); // OK,MyFn() is lavalue.so has an address
    
    Lvalue is an object which has a name and lifetime beyond a single expression. Rvalue object does not have name and lifetime is within a single expression. In other words you cannot obtain the memory address of an rvalue object but for lvalue objects you can. Rvalue objects are temporary while lvalues are permanent within it's scope. Note that lvalue can be modifiable or non-modifiable but in case of rvalue as you cannot obtain the memory address the question does not arise whether it is modifiable or non-modifiable. Rvalue can only be in the right side expression of an assignment operator but lvalue can be in either side. Return value of a function is rvalue when it returns by value.

    Prior to C++11 we could declare lvalue reference type wich could be bound to an lvalue object. We could bind an lvalue reference to an rvalue only if the reference was constant. There were no way to refer to an rvalue by a mutable reference. The syntax of declaring and initializing lvalue reference is:

    <typename>& <variable name> = <lvalue expression>;

    For example:
    Code:
        int i; // i is an modifiable lvalue
        int& j = i;//j is a non-constant lvalue reference type variable
        //int& k = 10; //Error:initial value of reference to non - const must be an lvalue
        const int& k = 10; //constant rference variable can be initialized with rvalue
    

    Rvalue Reference



    C++11 introduce new type of reference called rvalue reference which can be bound to rvalue object . The syntax for declaring and initializing an rvalue reference is similar to normal reference (lvalue reference) except using two ampersands instead of one :

    <typename>&& <variable name> = <rvalue expression>;

    For example:
    Code:
        int i; // i is a modifiable lvalue
        //int&& j = i;//j is a rvalue reference. Error: an rvalue reference cannot be bound to an lvalue
        int&& k = 10; //rvalue reference initialization binds to an rvalue only
        const int&& j = 10; //constant rvalue rference variable is initialized with rvalue
    
    Now see the following example code which will compile with the MyClass is defined in the previous code snippet.

    Code:
    int main()
    {
        MyClass obj(20);
        //MyClass&& myobj = obj;//lvalue cannot initialize rvalue reference. 
        MyClass&& myobj1 = MyClass(10); //rvalue reference is initialized with rvalue
        MyClass& myobj2 = MyClass(10); // lvalue reference can be initialized with rvalue
        
        return 0;
    }
    

    Move semantics



    Note that rvalue reference can only be bound to rvalues.Thus rvalue reference can be used to detect whether a value is a temporary object or not. Using this behavior of rvalue reference, move semantics introduced the way to detect the rvalue as arguments while copying objects. Two special member functions, alternative to copy constructor and copy assignment operator are introduced for that purpose.
    1. Move constructor - Move constructor is an overload of copy constructor where parameter type is mutable rvalue reference instead of lvalue reference. It is invoked when a temporary object (rvalue) is used to initialize a new object.
    2. Move assignment operator - Move assignment operator is an overload of copy assignment operator where parameter type is mutable rvalue reference instead of lvalue reference. It is invoked when a temporary object (rvalue) is used to initialize an existing object.
    Following sample program will show how those two special functions help to reuse memory allocated by a temporary object.

    Code:
    #include<vector>
    #include<iostream>
    using namespace std;
    class MyClass
    {
    private:
        static int instancecount;
        int* pIntArray;
        int Size;
    public:
        MyClass(int size)
        {
            pIntArray = new int[size]; // Allocating memory
            for (int i=0; i < size;i++)
                pIntArray[i] = i;
            Size = size;
            cout << "New instance of MyClass is created" << endl;
            cout << "Total Number of instances: " << ++instancecount << endl<<endl;
        }
        //Copy constructor will accept lvalue or rvalue as argument
        MyClass(const MyClass& obj)
        {
            Size = obj.Size;
            pIntArray = new int[Size]; // Allocating new memory
            for (int i=0; i < Size;i++)
                pIntArray[i] = obj.pIntArray[i];
            cout << "New instance of MyClass is created with the copy of another instance" << endl;
            cout << "Total Number of instances: " << ++instancecount << endl << endl;
        }
        // Move constructor will only accept rvalue object
        MyClass(MyClass&& obj)
        {
            Size = obj.Size;
            pIntArray = obj.pIntArray; // Reusing the memory from the temporary object.                             //Avoiding memory allocation and content copy
            obj.pIntArray = nullptr;//setting pointer to nullptr so that the destructor                             //does not delete the memory
            cout << "New instance of MyClass is created with the move of another instance" << endl;
            cout << "Total Number of instances: " << ++instancecount << endl << endl;
        }
        //Copy assignment operator will accept both lvalue and rvalue as argument
        MyClass& operator=(const MyClass& source)
        {
            if (this != &source)
            {
                delete pIntArray; // releasing old memory because size of new memory                         //may be different
                Size = source.Size;
                pIntArray = new int[Size]; // Allocating new memory
                for (int i = 0; i < Size;i++)
                    pIntArray[i] = source.pIntArray[i];
                cout << "Existing instance of MyClass is being overwriten with the copy assignment" << endl;
                
            }
            return *this;
        }
    
        // Move assignment operator will accept only rvalue argument
        MyClass& operator=(MyClass&& source)
        {
            if (this != &source)
            {
                delete pIntArray; // releasing old memory because size of new memory                         //may be different
                Size = source.Size;
                pIntArray = source.pIntArray; //Reusing the memory from the argument                     //object. Avoiding memory allocation and content copy
                source.pIntArray = nullptr;//setting pointer to nulptr so that the                                 //destructor does not delete the memory
                cout << "Existing instance of MyClass is being overwriten with the move assignment" << endl;
    
            }
            return *this;
        }
        ~MyClass()
        {
            cout << "One instance of MyClass is destroyed" << endl;
            cout << "Total Number of instances: " << --instancecount << endl<<endl;
        }
        
    };
    int MyClass::instancecount = 0;
    
    int main()
    {
        vector<MyClass> V1;
    /* Constructor and move Constructor both are called. Two MyClass object is created, 1st object is created and then a copy is created to pass the argument by value. Note the first instance is a temporary object, and will be destroyed immediately.*/
        V1.push_back(MyClass(1)); 
        MyClass obj1(10); // Constructor is called
        obj1 = MyClass(20); // Move assignment operator is called.
    
        return 0;
    }// Elements of the vector V1 and obj1 will be destroyed here
    
    
    Code:
    New instance of MyClass is created
    Total Number of instances: 1
    New instance of MyClass is created with the move of another instance
    Total Number of instances: 2
    One instance of MyClass is destroyed
    Total Number of instances: 1
    New instance of MyClass is created
    Total Number of instances: 2
    New instance of MyClass is created
    Total Number of instances: 3
    Existing instance of MyClass is being overwriten with the move assignment
    One instance of MyClass is destroyed
    Total Number of instances: 2
    One instance of MyClass is destroyed
    Total Number of instances: 1
    One instance of MyClass is destroyed
    Total Number of instances: 0
    
    If you look into the move constructor and move assignment operator closely, you will find that it is actually two specific case of function overloading where they varies in the argument type of rvalue reference and lvalue reference. Using rvalue references you can overload a function to differentiate on the basis of argument being normal object or temporary object. Following simplified code will demonstrate that:
    Code:
    #include <iostream>
    using namespace std;
    class MyClass
    {
    public:
        string Description;
        MyClass()
        {
            
            Description = "Lvalue";
        }
    };
    
    
    void DetectTemporary(MyClass& lvalue)
    {
        cout << "Argument is not a temporary object and description is: " << lvalue.Description.c_str() << endl;
    }
    
    void DetectTemporary(MyClass&& rvalue)
    {
        cout << "Argument is a temporary object and description is " << rvalue.Description.c_str() << endl;
    }
    
    MyClass GetRvalue()
    {
        MyClass obj;
        obj.Description = "Rvalue";
        return obj;
    }
    
    int main()
    {
        MyClass myobj;
        DetectTemporary(myobj);
        DetectTemporary(GetRvalue());
        return 0;
    }
    
    
    Code:
    Argument is not a temporary object and description is: Lvalue
    Argument is a temporary object and description is Rvalue
    
    Here one important to remember is that function arguments are lvalue as they have names. Argument type (& or &&) decides whether we can pass lvalue or not. When argument type is in the form of T&& we can only pass rvalue reference, in all other cases we can pass either lvalu or rvalue.

    We can also cast an lvalue to rvalue reference using static_cast:

    DetectTemporary(static_cast<MyClass&&>(myobj)); will give following output:

    Argument is a temporary object and description is Lvalue

    This can be done by template function std::move() which does the type casting

    DetectTemporary(std::move(myobj));

    Casting lvalue object to rvalue using static_cast or std::move() should be done only when you know the object will be no more used. Once you used the lvalue object as movable it's resource like heap memory is lost. That's why you should not use std::move() on objects you want to keep around.

    Following Swap function without using std::move copies the objects 3 times:

    template <typename T>
    void Swap(T& obj1, T& obj2)
    {
    T tmp(0);
    tmp = obj1;
    obj1 = obj2;
    obj2 = tmp;
    }

    Now the new swap function uses std::move() to move objects instead of expensive copy:

    template<typename T>
    void SwapNew(T& obj1, T& obj2)
    {
    T tmp(0);
    tmp = std::move(obj1);
    obj1 = std::move(obj2);
    obj2 = std::move(tmp);
    }

    Now if T is a vector<MyClass> of size n, 1st version will do 3*n time of expensive copy operation whereas the 2nd version will just do 3 move operation for 3 pointers to vector buffers.

    Rvalue reference and move semantics is a very important feature of C++11. Every expert programmer should get used to it. After all C++ is a great language to create efficient programs. Most of the STL functions have implemented move constructor and move assignment operator. If any class's copy constructor need to do deep copy should overload copy constructor and copy assignment operator for rvalue reference i.e. move constructor and move assignment operator.
     

Share This Page

  1. This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
    By continuing to use this site, you are consenting to our use of cookies.
    Dismiss Notice