Move Semantics

In the previous post on Rvalue References, we introduced lvalues, rvalues and temporary objects. The purpose of that post was to lay the groundwork for a discussion on move semantics, an important feature which was introduced in the C++11 standard.

We again consider the example of a Vector class for 3D geometry.

The definition file for such a class might look like this:

class Vector
{
public:
    const int Size = 3;

    Vector();
    virtual ~Vector();
    
    double operator[](int i) const;
    double& operator[](int i);
    Vector operator+(const Vector& other);

private:
    double* _data;
};

Memory to store the array is allocated in the constructor and deallocated in the destructor. Therefore, we should also provide a copy constructor and an assignment operator (in accordance with the rule of three):

    virtual ~Vector();
    Vector(const Vector& other);
    Vector& operator=(const vector& other);

Thanks to the use of operator overloading, we can now use the natural syntax for mathematical expressions, e.g,:

Vector x, y, z;
x[0] = 0.0; x[1] = 1.0; x[2] = 2.0;
y[0] = 3.0; y[1] = 4.0; y[2] = 5.0;
z = x + y;

After profiling, we typically find that the creation and calculation of z requires 3 allocations! The compiler can optimize this to 2 allocations if we combine the declaration and assignment as follows:

Vector z = x + y;

But we should only need 1 allocation.

Note that x+y is a temporary value. After it has been assigned to z it is thrown away. Why can’t we just ‘steal’ the pointer from x+y and ‘give’ it to z?

One possible solution in C++03 is to implement reference counting.

C++11 provides move semantics to overcome this problem. In order to make use of move semantics, we need to implement the following in our class:

  • Move constructor
  • Move assignment operator

For example:

// Move constructor
Vector(Vector&& other) noexcept
{
    _data = other._data;
    other._data = nullptr;
}

// Move assignment operator
Vector& operator=(Vector&& other) noexcept
{
    if (this != &other)
    {
        delete this->_data;
        this->_data = other._data;
        other._data = nullptr;
    }
    return *this;
}

When we assign a temporary object to variable, the compiler will use the move assignment operator instead of the traditional assignment operator. The difference here is that in our implementation of the move assignment operator, we don’t need to allocate storage. Instead, we take ownership of the memory that has already been allocated by the temporary object.

Summary

The rule of three states that if any of the following are provided in a class, they should all be implemented:

  • Destructor
  • Copy constructor
  • Copy assignment operator

In C++11, if we want to take advantage of move semantics, we should follow the rule of five:

  • Destructor
  • Copy constructor
  • Copy assignment operator
  • Move constructor
  • Move assignment operator

Move semantics can significantly streamline memory management.

This technique can be useful in engineering applications where we might typically want to overload mathematical operators such as +,-,*. Some examples include:

  • Vectors, matrices
  • Automatic differentiation
  • Polynomials