image0

As a remainder, an lvalue is an expression that can be on the left side of an operation. In the C times it could be characterized as "having an address" or "having a name". With C++, it is a bit more complicated. For example f() can be an lvalue if f returns a reference to something. On the other hand, an rvalue is an expression that is not an lvalue. For example "a+b" is not an lvalue. You cannot assign a value to "a+b". Well, except if you override operator+ to return a reference, again.

Move semantics and rvalue references are new concepts in C++11. I am going to cover them at the same time because they are linked. Basically, rvalue references are non-const references to rvalues (typically: objects that are going out of scope). On the other hand, the "move constructor" concept allows constructing an object by "stealing" the content of another object that is going to be destroyed.

Why introduce a new concept ?

If you construct an object from a temporary object, which by essence will be destructed soon afterwards, the basic way is to copy the content of the temporary object, then let it be destroyed. Imagine the overhead when the object is a huge matrix, or contains resources such as file handles or sockets in its members. In this case, it would seem much more intuitive to "steal" its resources. The newly allocated object's constructor would then really cheap, and the temporary object destructor should destroy nothing.

This is what those new concepts allow. This "stealing" constructor is called a move constructor, and it takes as parameter an "rvalue reference", which is a non const reference to a temporary object. This parameter really needs to be non const, since we are going to get its resources and update it from deleting them.

Of course you might think that you never have to use all of this, simply because you only transfer objects by reference when that have a big memory footprint or carry resources such as file handles. You might also always work on objects that do shallow copies, i.e. really contains just reference counted pointers to these big objects. Well in those cases, move constructors can be useful to simplify the existing code, and can improve the performance of existing code by introducing the new semantics of the move constructor.

Examples

Let's assume you have a class called Matrix that has some important memory impact. You can do some operations on matrixes, which return new Matrix objects. At the same time, you would want to minimize deep copy whenever possible, and work on existing matrixes when you can. In the following sample code, a matrix contains just an array of 10 integers. To be able to see easily the impact of move semantics, outputs are done when a "heavy" operation is done (allocating/freeing memory, copying data, ...). The move semantics have been put at the end of the class, and can be enabled or disabled at will.

Warning: I'm using an array of int values just as a sample of allocating and swapping resources in the move constructor. Things would obviously not be implemented this way in the real world. So let's say you have this stripped-down Matrix class:

class Matrix
{
public:
    Matrix()
    {
        std::cout << "  Constructing a new matrix. Allocating 10 ints" << std::endl;
        customData = new int[10];
    }

    ~Matrix()
    {
        if (customData != nullptr)
        {
            std::cout << "  Destructing a matrix. Freeing 10 ints." << std::endl;
            delete[] customData;
        }
    }

    Matrix(Matrix const& m)
    {
        std::cout << "  Copy constructing a matrix. Allocating 10 ints and copy all data." << std::endl;
        customData = new int[10];

        std::copy(m.customData, m.customData + 10, customData);
    }

    Matrix& operator= (Matrix const& m)
    {
        if (this != &m)
        {
            std::cout << "  Assigning a matrix. Copy all data." << std::endl;
            std::copy(m.customData, m.customData + 10, customData);
        }
        return *this;
    }

    Matrix& operator+=(Matrix const& m)
    {
        std::cout << "    Adding a matrix to another" << std::endl;
        return *this;
    }

    Matrix& operator*=(Matrix const& m)
    {
        std::cout << "    Multiplying a matrix to another" << std::endl;
        return *this;
    }

    Matrix operator+(Matrix const& m) const
    {
        Matrix result(*this);
        result += m;
        return result;
    }

    Matrix operator*(Matrix const& m) const
    {
        std::cout << "    Multiplying a matrix to another" << std::endl;
        Matrix result(*this);
        result *= m;
        return result;
    }
private:
    int *customData;
};

And you want to do a computation like: m1 + m4 * (m3 + (m1 * m2))

int main()
{
    Matrix m1, m2, m3, m4;

    {
        Matrix operation(m1);
        operation *= m2;
        operation += m3;
        operation *= m4;
        operation += m1;
    }

    return 0;
}

Here is the corresponding output:

Initialization
  Constructing a new matrix. Allocating 10 ints
  Constructing a new matrix. Allocating 10 ints
  Constructing a new matrix. Allocating 10 ints
  Constructing a new matrix. Allocating 10 ints
  Copy constructing a matrix. Allocating 10 ints and copy all data.
    Multiplying a matrix to another
    Adding a matrix to another
    Multiplying a matrix to another
    Adding a matrix to another
  Destructing a matrix. Freeing 10 ints.
  Destructing a matrix. Freeing 10 ints.
  Destructing a matrix. Freeing 10 ints.
  Destructing a matrix. Freeing 10 ints.
  Destructing a matrix. Freeing 10 ints.

We don't waste any memory on this example because all the computations are done "in place" in the matrix called "operation". Problem is: this is not really readable. Imagine we want to build more complex operations, things will eventually get really hard to follow. So let's turn to operator overloading in C++, our example becomes:

int main()
{
    Matrix m1, m2, m3, m4;

    {
        Matrix operation = m1 + m4 * (m3 + (m1 * m2));
    }

    return 0;
}

This is much more concise and readable than the former. But let's take a look at the output:

Constructing a new matrix. Allocating 10 ints
Constructing a new matrix. Allocating 10 ints
Constructing a new matrix. Allocating 10 ints
Constructing a new matrix. Allocating 10 ints
  Multiplying a matrix to another
Copy constructing a matrix. Allocating 10 ints and copy all data.
  Multiplying a matrix to another
Copy constructing a matrix. Allocating 10 ints and copy all data.
  Adding a matrix to another
  Multiplying a matrix to another
Copy constructing a matrix. Allocating 10 ints and copy all data.
  Multiplying a matrix to another
Copy constructing a matrix. Allocating 10 ints and copy all data.
  Adding a matrix to another
Destructing a matrix. Freeing 10 ints.
Destructing a matrix. Freeing 10 ints.
Destructing a matrix. Freeing 10 ints.
Destructing a matrix. Freeing 10 ints.
Destructing a matrix. Freeing 10 ints.
Destructing a matrix. Freeing 10 ints.
Destructing a matrix. Freeing 10 ints.
Destructing a matrix. Freeing 10 ints.

Each operation is allocating a new matrix ! We are now using twice as much memory as the in place version. What if we want both readability AND performance ? That's where move semantics comes to help. Suppose you add these members to your existing Matrix class. That's right, you don't have to modify existing code to take advantage of move semantics.

Matrix(Matrix && a)
{
    customData = a.customData;
    a.customData = nullptr;
}

Matrix operator*(Matrix && m) const
{
    std::cout << "    Multiplying a matrix with a temporary matrix" << std::endl;
    Matrix result(std::forward< Matrix>(m));
    result *= *this;
    return result;
}

Matrix operator+(Matrix && m) const
{
    std::cout << "    Adding a matrix with a temporary matrix" << std::endl;
    Matrix result(std::forward< Matrix>(m));
    result += *this;
    return result;
}

Matrix& operator=(Matrix && m)
{
    if (this != &m)
    {
        std::cout << "  Assigning a matrix from a temporary. Free our current data." << std::endl;
        delete[] customData;
        customData = m.customData;
        m.customData = nullptr;
    }
    return *this;
}

We don't need to change the contents of the "main" function. Here's the corresponding output:

Constructing a new matrix. Allocating 10 ints
Constructing a new matrix. Allocating 10 ints
Constructing a new matrix. Allocating 10 ints
Constructing a new matrix. Allocating 10 ints
  Multiplying a matrix to another
Copy constructing a matrix. Allocating 10 ints and copy all data.
  Multiplying a matrix to another
  Adding a matrix with a temporary matrix
  Adding a matrix to another
  Multiplying a matrix with a temporary matrix
  Multiplying a matrix to another
  Adding a matrix with a temporary matrix
  Adding a matrix to another
Destructing a matrix. Freeing 10 ints.
Destructing a matrix. Freeing 10 ints.
Destructing a matrix. Freeing 10 ints.
Destructing a matrix. Freeing 10 ints.
Destructing a matrix. Freeing 10 ints.

Basically, we're back to the memory footprint of the first, unreadable computation style. You might notice that the code for the + and * operators are similar for const references and rvalues references. On a real life example, you would want to use templates for operators, like this:

template < typename t="t" >
Matrix operator*(T && m) const
{
    Matrix result(std::forward< t >(m));
    result *= *this;
    return result;
}

Pitfalls

As the C++ gurus say "C++ will never prevent a developer from shooting himself in the foot". There are some pitfalls that you might want to avoid when working with rvalue references:

Calling the wrong override

void g(A const& a)
{
  // Do something with a
}

void g(A && a)
{
  // Do something else with a
}



void f(A && a)
{
  g(a);
}

This is not obvious at all when you're not yet used to rvalue references, but the override that gets called by f is g(const&). This is the case because, even if a's type is "rvalue reference of an A", it is still an lvalue in the context of the function: we can know its address, and more importantly we still have access to it after g(a). If we were to call g(a+b) for example, the rvalue reference overload would be called because a+b is not a variable name, and would be out of scope after the right parenthesis of g(a+b).

In other words, we must never forget what a rvalue is: an object that is going out of scope, an object which we can never refer to anymore. This is not the case inside f, where a is a parameter, and not a temporary value (even if it really references a temporary value).

Difficulty to see improvements with small samples

gcc already optimizes much with Return Value Optimization and copy elision. It may be difficult to test rvalue references with small tests because of this. In real code, though, the benefits are real if you remember to always use rvalue references when it is possible and meaningful to do so.

Going further

See this article that goes into the deepest details of the move and rvalue references semantics here.


Comments

comments powered by Disqus