C++ Move Semantics
Jun 5, 2016
10 minutes read

All function parameters are lvalues even though their type may be defined as rvalue reference. Oh, cool! Wait, what? While the terminology can be confusing move semantics is one of the most crucial features of C++ as it allows you to write more effecient and high performing code. It’s important to understand when and how to apply it.

Introduction

Consider the example of me having a book that I want to give to you because I’m finished with it (and because books are too important to just throw away). Now, imagine that I would share this book with you by first copy every letter by hand and then giving you that newly made copy. That’s a really labour intensive job when instead I could’ve just given you my copy directly; that would’ve been a much more efficient operation. In C++ that’s what move semantics allows us to do.

Move semantics is about tranferring ownership of resources from one object to another. This is different from copy semantic which give you a duplicate of the original resource. A move operation can be a lot cheaper (i.e. faster) than a copy operation so it’s important to consider in order to write the best performing code.

The feature that makes this all possible is a new reference type, rvalue reference, that was introduced in C++11 back in August 2011.

Value Categories

In C++ every expression is either an rvalue or an lvalue; which formally denotes which value category they belong to. rvalues and lvalues have been part of the language since, well, forever and are loosely defined as:1

  • an lvalue is named and/or something you can take the address of (has identity)
  • an rvalue is unnamed and/or something you can’t take the address of (has no identity)

Historically they got their names for where they can appear in an assignment operation. lvalue, then, meaning “left hand or right hand side of assignment” and rvalue meaning “right hand side only of assignment”. If we generalize further rvalue means a temporary value.

Example:

// x and y are lvalues.
int x = 5;
int y = 10;

// z is an lvalue, (x * y) is an rvalue.
int z = (x * y);

(x * y) = 15;
// error: expression is not assignable
// (can't to assign to an rvalue - a temporary).

The reason it’s not possible to assign to an rvalue is that it only exist during the evaluation of the expression and goes out of scope at the first semicolon.

References

Traditionally when we refer to a reference we mean something like this:

int x = 5;
int& y = x;  // y is a lvalue reference.

That’s still a “normal” reference, but as the comment indicates it’s now referred to as a lvalue reference. That is to distinguish it from the new reference type: rvalue reference.

Where X is some type, references takes the following form:

  • lvalue reference: X&
  • rvalue reference: X&&

An rvalue reference will only bind to an rvalue, that is a temporary. An lvalue reference, however, will bind to an lvalue, or to an rvalue if the lvalue reference is const.

int x = 5;
int y = 10;

int& z = x;              // lvalue reference 'y' binds to lvalue 'x'.

const int& r = (x * y);  // lvalue-rererence-to-const binds to rvalue.

int&& rr = (y * x);      // rvalue reference binds to rvalue (a temporary).

int&& rr = y;
// error: rvalue reference to type 'int' cannot bind to lvalue of type 'int'
// (can't assign a lvalue to a rvalue reference)

While there is lvalue references and rvalue references there’s no such thing as a reference to a reference. Except when there is.

A reference to a reference can happen during type deduction. Type deduction happens for template type parameters and auto declaration. For instance:

// rvalue reference to template type parameter. Here the compiler
// will employ type deduction to determine the type of 'x'. Note: x
// itself is a lvalue even though its type may be rvalue referece.
template <typename T>
void Foo(T&& x)
{
    // ...
}

// The type of 'x' needs to be type deduced, it goes through the same
// process as for template type deduction.
auto&& x = SomeFunction();

Let’s use function Foo as an example what happens during type deduction.
(X denotes some user type):

X x;

// Call Foo with an lvalue, T is deduced to X&.
Foo(x);

// Call Foo with an rvalue, T is deduced to X.
Foo(makeX());

If we look at the first example where T is deduced to X&, it means we’ll end up with a reference to a reference, Foo will be instantiated as:

Foo(X& && x)
{
    // ...
}

What happens in this case?

Whenever the compiler comes across a reference to a reference it will employ a procedure known as reference collapsing. That is actually very strightforward process; only if both references are rvalue references will the result of the reference collapsing ever be a rvalue reference, otherwise the result is a lvalue reference. Say what? Let’s look at the possible result of the type deduction:

Foo(& &)   - result: Foo(&)  - lvalue reference
Foo(& &&)  - result: Foo(&)  - lvalue reference
Foo(&& &)  - result: Foo(&)  - lvalue reference
Foo(&& &&) - result: Foo(&&) - rvalue reference

The C++ Standard refers to a type deduced rvalue reference as a forwarding reference2. However I find that introducing yet another reference term is more confusing than helpful, particularly when you view them next to each other:
(X referes to some user type)

// "forwarding reference"
template <typename T>
void Foo(T&& x)
{
    // ...
}

// rvalue reference
void Bar(X&& x)
{
    // ...
}

Both look like a rvalue reference, because they are. So I like to refer to them as simply as rvalue reference and where the distinction needs to be made, type deduced rvalue reference.

The important thing about rvalue reference is that they can be moved from. You cannot safely do that from lvalue references since they might be used after the move operation.

Move Semantics

Move semantics for objects are implemented with the move constructor and the move assignment operator, siblings of the traditional copy constructor and copy assignment operator.

class Foo {
public:
    Foo(const Foo&);              // Copy constructor.
    Foo& operator=(const Foo&);   // Copy assignment operator.

    Foo(Foo&&);                   // Move constructor.
    Foo& operator=(Foo&&);        // Move assignment operator.
};

The move constructor is called when an object is initialized with an xvalue (eXpiring value), which is another value category which has the same properties as a rvalue but it also has an identity.

Let’s look at a concrete example. Let’s say we have an abstraction for a buffer. This buffer is allocated on the free store, so we just store a pointer and a size in the object.

class Buffer {
public:
    Buffer(int n) :
        size(n)
    {
        ptr = new unsigned char[size];
    }

    ~Buffer()
    {
        size = 0;
        delete [] ptr;
        ptr = nullptr;
    }

    // Move constructor.
    Buffer(Buffer&& b) :
        size(b.size),
        ptr(b.ptr)
    {
        // Invalidate object we've just moved from.
        b.size = 0;
        b.ptr = nullptr;
    }

private:
    int size { 0 };
    unsigned char* ptr { nullptr };
};

The move constructor will be called for any rvalue arguments. We can also give a strong hint to the compiler that we want a move to happen. The question is how?

Remember the introductory statement about parameters being lvalues depite their type being rvalue reference? In order to be able to move from them we have to convert the named lvalue parameter into a type that can be bound to an rvalue - which is exactly what an xvalue is for.

std::move() is how we accomplish that; it indicates that the argument passed to it may be moved from. The result of the operation is an xvalue. It’s a compile time construct only (static cast), so there’s no run-time costs for it.

Buffer b0 { 5 };
Buffer b1 { std::move(b0) };

The second line move contructs b1 by just reassigning the pointer from the original object.

This example also highlights an important potential pitfall, mentioned at the end of the last section, to be aware of. b0 is an lvalue and the fact that we have now moved its resources from underneath it means that it’s no longer in a valid state, yet we can still access it. This is why you normally only move from rvalues (temporaries) that you will no longer access after the move call. Only move from lvalues when you are certain they will not be used further.

It’s also important to remember that the destructor will still be called on the object that had its resources moved from it, so it’s important it’s left in a destructable state.

The prototypical example of moving for “normal” functions is the the swap operation:

template <typename T>
void Swap(T& a, T& b)
{
    T tmp { std::move(a) };
    a = std::move(b);
    b = std::move(tmp);
}

Not all objects benefits moving. Typically we want to support move operations for objects which has resources allocated on the free store, and where doing a copy is a deep copy. Our Buffer fits this paradigm neatly, it’s managing a resource with potentially large amount of elements but only keeps a pointer to it. Moving it is therefor a quick operation - just a pointer reassignment. If we were to copy it we’d first have to allocate a new memory area on the free store, then copy each element into it.

Perfect Forwarding

rvalue references also enables a language feature known as perfect forwarding. In essence, perfect fowarding preserves the value category of the arguments as they are passed to a different function. This means that the second function receives the arguments exactly like the first function did.

This is accomplished by another compile type construct, std::forward(). This is used in conjunction with type deduced rvalue references. For example:

template <typename T>
void Foo(T&& p)
{
    SomeFunction(std::forward<T>(p));
}

Here ‘p’ is passed on to ‘someFunction()’ exactly as it was passed to ‘Foo’. std::foward() is different from std::move() in that the std::move() always produces a value that can be bound to an rvalue reference, whereas std::foward() will only produce an rvalue if its argument is an rvalue (otherwise a lvalue).

This is especially useful with templates and higher-order programming, where functions and their parameters are passed as aguments to (or returned from) other functions. You want be certain they are passed along in their original “state”.

Summary

Move semantics is a crucial language feature for writing high performance code. At the heart of it is the idea of replacing expensive copy operations with cheaper move operations, especially those associated with temporary objects.

Moving is of most benefit when the data held by the object has been free store allocated and when the copy operation is a deep copy.

  • Move semantics makes it possible to write more efficient code
  • rvalue reference is a new reference type that identifies an object as movable
  • Reference collapsing only yields a rvalue reference if both references are rvalue references
  • Type deduced rvalue reference is a rvalue reference that depends on a template type parameter (“forwarding reference”)
  • Use std::move() when you want to transfer resource ownership between objects
  • Use std::foward() when you want to preserve value category for arguments passed on to other function
  • Moving is safe for rvalues, it’s generally not save for lvalues
  • The Standard Template Library (STL) fully supports move semantics

Have you seen any significant performance gains by enabling move semantics in your codebase? Share your thoughts and experiences in the comments below!


  1. The formal description is somewhat more involved, but for the purpose of this post this simplified description suffice. [return]
  2. ยง14.8.2.1 / 3: “A forwarding reference is an rvalue reference to a cv-unqualified template parameter.” [return]


comments powered by Disqus