C++ Move Semantics
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 reference
2. 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!