Move semantics | C++

/Move semantics | C++

Introduction

Move semantics is a new way of moving resources around in an optimal way by avoiding unnecessary copies of temporary objects, based on rvalue references. C++11 defines two new functions in service of move semantics: a move constructor, and a move assignment operator. Whereas the goal of the copy constructor and copy assignment is to make a copy of one object to another, the goal of the move constructor and move assignment is to move ownership of the resources from one object to another (which is much less expensive than making a copy). Defining a move constructor and move assignment work analogously to their copy counterparts. However, whereas the copy flavors of these functions take a const l-value reference parameter, the move flavors of these functions use non-const r-value reference parameters.

 

rvalue, lvalue, and &&

To understand how move semantics work, you need to understand the concepts of rvalues and lvalues:

  • an lvalue is an expression whose address can be taken, a locator value. Anything you can make assignments to is an lvalue.
  • an rvalue is an unnamed value that exists only during the evaluation of an expression i.e. an expression is an rvalue if it results in a temporary object.
  • the && operator is new in C++11, and is like the reference operator (&), but whereas the & operator an only be used on lvalues, the && operator can only be used on rvalues.
// To illustrate this, consider the following examples:
int a = 1; // here, a is an lvalue (and the '1' is an rvalue)

// getRef() is an lvalue - returns a reference to a global variable, so it's returning a value that is stored in a permanent location.
int x;
int& getRef () 
{
  return x;
}
getRef() = 4;


// Returns a local variable, constructed inside the function. It is returning an rvalue
string getName() { 
   string s = "Hello world";    return s; 
} 

// as getName() returns an rvalue, assigning getName()'s result to an rvalue reference is possible
string&& name1 = getName(); 

// you can also getName() it to a value object, without any copying of string data being required
string name2 = getName();

 

Need of move semantic

When doubleValues is called, it constructs a vector, new_values, and fills it up. When this function hit the return statement, the entire contents of new_values must be copied to a temporary variable. In principle, there could be up to two copies here: one into a temporary object to be returned, and a second when the vector assignment operator runs on the line v = doubleValues(v);. The first copy may be optimized away by the compiler automatically, but there is no avoiding that the assignment to v will have to copy all the values again, which requires a new memory allocation and another iteration over the entire vector.

One can find ways to avoid this kind of problem–for example, by storing and returning the vector by pointer, or by passing in a vector to be filled up. The thing is, neither of these programming styles is particularly natural.

Since the object returned from doubleValues is a temporary value that’s no longer needed. When you have the line v = doubleValues( v ), the result of doubleValues( v ) is just going to get thrown away once it is copied! In theory, it should be possible to skip the whole copy and just pilfer the pointer inside the temporary vector and keep it in v. In C++11, move semantics can be used to move and avoid the copy.

That’s what rvalue references and move semantics are for! Move semantics allows you to avoid unnecessary copies when working with temporary objects that are about to evaporate, and whose resources can safely be taken from that temporary object and used by another. Move semantics relies on a new feature of C++11, called rvalue references.

#include <iostream>
 
using namespace std;
 
vector<int> doubleValues (const vector<int>& v)
{
  vector<int> new_values;
  new_values.reserve(v.size());
  for (auto itr = v.begin(), end_itr = v.end(); itr != end_itr; ++itr )
  {
      new_values.push_back( 2 * *itr );
  }
  return new_values;
}
 
int main()
{
  vector<int> v;
  for ( int i = 0; i < 100; i++ )
  {
      v.push_back( i );
  }
  v = doubleValues( v );
}

 

Detecting temporary objects with rvalue references

An rvalue reference is a reference that will bind only to a temporary object. t. Rvalue references use the && syntax instead of just &, and can be const and non-const.

const string&& name = getName();
string&& name       = getName();

 

The most important thing about lvalue references vs rvalue references is what happens when you write functions that take lvalue or rvalue references as arguments. First printReference() function taking a const lvalue reference will accept any argument that it’s given, whether it be an lvalue or an rvalue, and regardless of whether the lvalue or rvalue is mutable or not. However, in the presence of the second overload, printReference taking an rvalue reference, it will be given all values except mutable rvalue-references.

printReference (const String& str)
{
  cout << str;
}

printReference (String&& str)
{
  cout << str;
}

string name("IN");
// calls the first printReference function, taking an lvalue reference
printReference(name); 
// calls the second printReference function, taking a mutable rvalue reference
printReference(getName());

 

Special member functions

In old C++, there were four special member functions. Now with C++11’s two move semantics functions, there’s six:

  1. Default constructor
  2. Destructor
  3. The two copy special member functions
    • Copy constructor
    • Copy assignment operator
  4. The two move special member functions
    • Move constructor
    • Move assignment operator

As you know, in C++ when you declare any constructor, the compiler will no longer generate the default constructor for you. The same is true here: adding a move constructor to a class will require you to declare and define your own default constructor. On the other hand, declaring a move constructor does not prevent the compiler from providing an implicitly generated copy constructor, and declaring a move assignment operator does not inhibit the creation of a standard assignment operator.

 

Supporting move semantics

To support moving, you class needs:

  • a Move constructor of the form C::C(C&& other);
  • a Move assignment operator of the form C& C::operator=(C&& other);

These will often be automatically defined+deleted by the compiler, and even when you do need to define/delete them, often it’s sufficient to just define them to use the default implementation or delete them.

A move constructor, like a copy constructor, takes an instance of an object as its argument and creates a new instance based on the 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. If the field is a primitive type, like int, we just copy it. It gets more interesting if the field is a pointer: here, rather than allocate and initialize new memory, we can simply steal the pointer and null out the pointer in the temporary object! We know the temporary object will no longer be needed, so we can take its pointer out from under it. Consider below example

class ArrayWrapper
{
public:
  // Constructor
  ArrayWrapper (int n)
      : _p_vals(new int[n])
      , _size(n)
  {}

  // Move constructor
  ArrayWrapper (ArrayWrapper&& other)
      : _p_vals( other._p_vals  )
      , _size( other._size )
  {
    other._p_vals = NULL;
    other._size = 0;
  }

  // Copy constructor
  ArrayWrapper (const ArrayWrapper& other)
      : _p_vals( new int[other._size] )
      , _size(other._size)
  {
    for ( int i = 0; i < _size; ++i )
    {
      _p_vals[ i ] = other._p_vals[ i ];
    }
  }
  
  ~ArrayWrapper ()
  {
    delete [] _p_vals;
  }
 
private:
  int *_p_vals;
  int _size;
};

In move constructor, other._p_vals is set to NULL . The reason is the destructor–when the temporary object goes out of scope, just like all other C++ objects, its destructor will run. When its destructor runs, it will free _p_vals. The same _p_vals that we just copied! If we don’t set other._p_vals to NULL, the move would not really be a move–it would just be a copy that introduces a crash later on once we start using freed memory. This is the whole point of a move constructor: to avoid a copy by changing the original, temporary object!

Another example which illustrates how to implement move semantic. In this example – need to implement move semantics because of the pointer used in the class and also have a bunch of other class members which can all be moved safely, then to move them within the move constructor + move assignment operator you can simply use std::move, i.e. if MyClass above also had a member string myName.

class MyClass {

  int* buffer = nullptr;
  string myName;

public:

  // Move constructor
  MyClass(MyClass&& other) {
    buffer = other.buffer;
    other.buffer = nullptr;
    myName = std::move(other.myName);
  }

  // Move assignment operator  
  MyClass& MyClass::operator=(MyClass&& other) {
    if(this != &other) {
       if(buffer) {
          delete buffer;
       }
       // Steal data from the other object coming in as an rvalue reference, 
       buffer = other.buffer;
       other.buffer = nullptr;

       myName = std::move(other.myName);
    }

    return *this;
  }

  ~MyClass {
    if(buffer) {
        delete buffer;
     }
  }
};

 

September 14th, 2019|Categories: Programming|Tags: |
avatar
  Subscribe  
Notify of