Tags

  • AWS (7)
  • Apigee (3)
  • ArchLinux (5)
  • Array (6)
  • Backtracking (6)
  • BinarySearch (6)
  • C++ (19)
  • CI&CD (3)
  • Calculus (2)
  • DesignPattern (43)
  • DisasterRecovery (1)
  • Docker (8)
  • DynamicProgramming (20)
  • FileSystem (11)
  • Frontend (2)
  • FunctionalProgramming (1)
  • GCP (1)
  • Gentoo (6)
  • Git (15)
  • Golang (1)
  • Graph (10)
  • GraphQL (1)
  • Hardware (1)
  • Hash (1)
  • Kafka (1)
  • LinkedList (13)
  • Linux (27)
  • Lodash (2)
  • MacOS (3)
  • Makefile (1)
  • Map (5)
  • MathHistory (1)
  • MySQL (21)
  • Neovim (10)
  • Network (66)
  • Nginx (6)
  • Node.js (33)
  • OpenGL (6)
  • PriorityQueue (1)
  • ProgrammingLanguage (9)
  • Python (10)
  • RealAnalysis (20)
  • Recursion (3)
  • Redis (1)
  • RegularExpression (1)
  • Ruby (19)
  • SQLite (1)
  • Sentry (3)
  • Set (4)
  • Shell (3)
  • SoftwareEngineering (12)
  • Sorting (2)
  • Stack (4)
  • String (2)
  • SystemDesign (13)
  • Terraform (2)
  • Tree (24)
  • Trie (2)
  • TwoPointers (16)
  • TypeScript (3)
  • Ubuntu (4)
  • Home

    C++ rvalue references and move semantics for beginners

    Published Oct 21, 2019 [  C++  ]

    The logic behind rvalues is that in C++ you will find such temporary, short-lived values that you cannot alter in any way.

    int x = 666;                    // (1)
    int y = x + 5;                  // (2)
    
    std::string s1 = "hello ";
    std::string s2 = "world";
    std::string s3 = s1 + s2;       // (3)
    
    std::string getString() {
      return "hello world";
    }
    std::string s4 = getString();   // (4)
    

    Introducing the magic of rvalue references

    The traditional C++ rules say that you are allowed to take the address of an rvalue only if you store it in a const variable. More technically, you are allowed to bind a const lvalue to an rvalue.

    int& x = 666;       // Error
    const int& x = 666; // OK
    

    C++0x has introduced a new type called rvalue reference, denoted by placing a double ampersand && after some type. Such rvalue reference lets you modify the value of a temporary object: it’s like removing the const attribute.

    std::string   s1     = "Hello ";
    std::string   s2     = "world";
    std::string&& s_rref = s1 + s2;    // the result of s1 + s2 is an rvalue
    s_rref += ", my friend";           // I can change the temporary string!
    std::cout << s_rref << '\n';       // prints "Hello world, my friend"
    

    Move semantics

    Move semantics is a new way of moving resources around in an optimal way by avoid unnecessary copies of temporary objects, based on rvalue references.

    class Holder
    {
      public:
    
        Holder(int size)         // Constructor
        {
          m_data = new int[size];
          m_size = size;
        }
    
        ~Holder()                // Destructor
        {
          delete[] m_data;
        }
    
      private:
    
        int*   m_data;
        size_t m_size;
    };
    

    Rule of Three

    If your class defines one or more of the following methods, it should probably explicitly define all three

    • destructor
    • copy constructor
    • copy assignment operator

    Implementing the copy constructor

    The copy constructor is used to create a new object from another existing object.

    Holder h1(10000); // regular constructor
    Holder h2 = h1;   // copy constructor
    Holder h3(h1);    // copy constructor (alternate syntax)
    
    Holder(const Holder& other)
    {
      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;
    }
    

    Implementing the assignment operator

    Holder h1(10000);  // regular constructor
    Holder h2(60000);  // regular constructor
    h1 = h2;           // assignment operator
    
    Holder& operator=(const Holder& other) 
    {
      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)
    }
    

    The limitations of our current class design

    Holder createHolder(int size)
    {
      return Holder(size);
    }
    

    It returns a Holder object by value. We know that when a function returns an object by value, the compiler has to create a temporary object (rvalue). Our Holder is a heavy-weight object due to its internal memory allocation, which is a very expensive task: returning such things by value with our current class design would trigger multiple expensive memory allocations.

    int main()
    {
      Holder h = createHolder(1000); // Copy constructor
      h = createHolder(500);         // Assignment operator
    }
    

    Implementing move semantics with rvalue references

    The idea of move semantics is to add new versions of the copy constructor and assignment operator so that they can take a temporary object in input to steal data from.

    Rule of Five

    Any class for which move semantics are desirable, has to declare two additional member functions

    • the move constructor - to constructor new objects by stealing data from temporaries
    • the move assignment operator - to replace existing objects by stealing data from temporaries

    Implementing the move constructor

    Holder(Holder&& other)     // <-- rvalue reference in input
    {
      m_data = other.m_data;   // (1)
      m_size = other.m_size;
      other.m_data = nullptr;  // (2)
      other.m_size = 0;
    }
    

    Implementing the move assignment operator

    Holder& operator=(Holder&& other)     // <-- rvalue reference in input  
    {  
      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;
    }
    
    int main()
    {
      Holder h1(1000);                // regular constructor
      Holder h2(h1);                  // copy constructor (lvalue in input)
      Holder h3 = createHolder(2000); // move constructor (rvalue in input) (1) 
    
      h2 = h3;                        // assignment operator (lvalue in input)
      h2 = createHolder(500);         // move assignment operator (rvalue in input)
    }
    

    Can I move lvalues?

    int main()
    {
      Holder h1(1000);     // h1 is an lvalue
      Holder h2(h1);       // copy-constructor invoked (because of lvalue in input)
    }
    
    int main()
    {
      Holder h1(1000);           // h1 is an lvalue
      Holder h2(std::move(h1));  // move-constructor invoked (because of rvalue in input)
    }
    

    References: