Tuesday, December 22, 2015

r-value references and move semantics

This post discusses different types of expressions and references that can exist in C++11. In particular, it explains rvalue references that are new in C++11. It then touches on uses of std::move and std::forward, outlining appropriate and inappropriate usages.


Expressions: lvalues vs rvalues.


In C++, an expression is a sequence of operators and operands. An expression has a value, which can be classified into one of several value categories. For simplicity, we’ll talk about the most relevant of the value categories: lvalues and rvalues.


An lvalue expression is named so because most of the time it can appear on the left hand side of an assignment operation. This provides a good intuition when trying to decide whether an expression at hand is an lvalue expression. More formally, an lvalue expression is an expression that refers to a memory address. Specifically, if you can take an address of an expression, then it is an lvalue expression. You can think of lvalues as expressions that have a name. Most of the time they are just regular variables. Some examples of lvalue expressions are the following:


int n;
n = 3;          // n is an lvalue expression


int a[2];
a[0] = n;       // a[0] is an lvalue expression


int* p = &a[0];
*p = 5;         // *p is an lvalue expression
*++p = 7;       // *++p is an lvalue expression


An rvalue expression, on the other hand, is simply not an lvalue. These are expressions that can only appear on the right-hand side of an assignment operator. They are not assignable and do not refer to a specific memory location. Some examples of rvalue expressions are the following:


int n = 3;       // 3 is an rvalue expression
n = Function();  // Function() is an rvalue expression


int a, b;
n = a + b;       // a + b is an rvalue expression.


c[0] = n;        // n is _not_ an rvalue expression.
                // It’s still an lvalue, even though it’s on
                // the right hand side of the assignment.


The last example is an important concept. Just because an expression is on the right hand side of an assignment, does not make it an rvalue. That is, lvalues can appear on either left- or right- hand sides of an assignment operation. However, rvalues can only appear on the right hand side.


These are just two types of expressions that exist. There are more of them (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2010/n3055.pdf), but for the purpose of this document the important distinction we need to make is between lvalue and rvalue expressions.


Variable types: lvalue references vs rvalue references


When referring to lvalues and rvalues, we talk about expressions. Now let’s talk about variables. Variables have types. Some of these types may refer to expressions. These variables are, of course, references. There are a couple of familiar types of references:


  • lvalue references to non-const. These are references that can refer only to lvalue expressions. They, in fact, refer to objects, which is exactly what an lvalue expression represents. For example,


int a = 5;        // a has a type int
int& ref = a;     // ref has a type lvalue reference to int.
                 // It refers to an lvalue, |a|.


int& bad_ref = 5; // This line will not compile. Since 5 is an
// rvalue expression, an lvalue reference to
// non-const cannot bind to it.


  • lvalue references to const. Similar to lvalue references to non const, lvalue references to const can also refer to lvalues. However, additionally they can refer to rvalues as well (since they aren’t modifiable, this is OK). In fact, lvalue references to const extend the lifetime of temporaries until they, themselves, are destroyed. For example,


int a = 5;               // a has a type int
const int& ref = a;      // ref has a type lvalue reference to
                        // const int

struct Foo {};
const Foo& good_ref = Foo(); // good_ref has a type lvalue
// reference to const int.
// This is OK, since it’s a
// const ref. The lifetime of
// the temporary object Foo()
// is extended until good_ref
// goes out of scope.


As an aside, it is an important feature of C++ that lvalue references to const extend the lifetime of temporaries to which they are bound. We use these types of tricks all the time. Consider the following example:


std::string GenerateString() {
 …
}


const std::string& s = GenerateString();
// Work with s.


The call to GenerateString produces a temporary std::string. It would normally be destroyed at the end of the expression, unless it is copied. However, since it’s bound to an lvalue reference to const, the lifetime of this temporary is extended, which allows us to work with it after the GenerateString line is executed.


C++11 and even more reference types


Now, without C++11,
  • we have types that can refer to lvalue expressions only (lvalue references to non-const)
  • we have types that can refer to either lvalue or rvalue expressions (lvalue references to const)


C++11 introduces a natural extension to this: a type that can refer to rvalue expressions only, an rvalue reference. This is written as a double ampersand after a type:


MyType&& ref = …;  // ref has a type rvalue reference
// (to non-const).


This type of reference can only refer to rvalue expressions and will simply refuse to bind to lvalue expressions. Consider a previous example, with lvalue references changed to rvalue references:


int a = 5;              // a has a type int
int&& ref = a;          // This won’t compile! rvalue
// references cannot bind to lvalues
// (and a, in this case, is an lvalue).


int&& another_ref = 5;  // This is now fine! another_ref
// refers to an rvalue expression 5.
++another_ref;          // It’s also a reference to non-const,
// so it’s modifiable! It now refers
// to “6”


In the previous example, lvalue reference to non-const could bind to the variable |a|, but could not bind to the expression 5. Now the positions are reversed. Since rvalue references can only refer to rvalues, ref refuses to bind to the variable |a| (it’s an lvalue), but binds to 5 without issues. Rvalue references, like lvalue references to const, extend the lifetimes of the expressions to which they refer. Additionally, since this is a reference to non-const, you can modify the value of the expression that it is referring to.


As a side note, you can also have a rvalue reference to const, but it loses much of its power. In fact, I would say that if you must have an rvalue reference to const, you might as well use an lvalue reference to const. It saves you one character stroke, and it is understood by far more programmers.


So what?


It turns out the fact that rvalue references refer to rvalues and the fact that they are modifiable allows us to write pretty efficient code in some situations.


Consider the following code with a new rvalue reference constructor (also known as a move constructor):


struct MyClass {
MyClass(MyClass&& other) {
  …
}
};


There are two pieces of information we have within this constructor. First, we know that the value passed to it was an rvalue. For all intents and purposes, we can pretend that the value was a temporary (or at least the calling code wants us to believe it was a temporary). Second, we know that we can modify the argument. This allows this constructor to be more efficient in some situations. Consider the following,


struct MyClass {
MyClass(MyClass&& other) {
  large_vector_.swap(other.large_vector_);
}


private:
 std::vector<int> large_vector_;
};


With a typical copy constructor, we would have to copy a potentially large vector. However, with a move constructor, no copies are required for this vector. We know |other| is going away, and we can change it any way we want. So, we can afford to simply swap the vector with our own (empty) vector. This operation is typically much cheaper than a full copy. That’s the power of move constructors: efficient moving of containers and pointers.


However, before writing a move constructor for every type that you have ever written, also consider the following example:


struct MyClass {
MyClass(MyClass&& other) {
  …
}


private:
 int large_array_[500];
};


It’s a similar class, but it has an array instead of a vector. What can a move constructor do here that is more efficient than a copy constructor? Not much. In fact, the move constructor in this case would have to do exactly the same thing as a copy constructor. The reason is that the memory for the large array can’t be “moved”. There are no pointers to copy, no vector to swap. We have to do an element by element copy (or more likely a memcpy). However, this is exactly what a copy constructor would do as well. Considering this, a move constructor is actually unnecessary in this case; a copy constructor will work in more situations and do the same operations.


Coercing the type system (std::move)


So far, everything we talked about only involved the core C++11 language. The basic summary is that whenever you have an rvalue, it can be bound to an rvalue reference. This includes variables and functions parameters alike, including constructor and assignment operator parameters. However, we can also get into situations where we would really like to get an rvalue, but we simply don’t have one:


void function() {
 MyClass my_class;
 // Construct and initialize a non-trivial type.
 // …
 set_new_class(my_class);
}


After we make a call to set_new_class, the enclosing block also ends, which means |my_class| is not going to be used. In fact, we would really like to say that it might as well be a temporary. However, as the code stands right now, during the call to set_new_class, |my_class| is an lvalue. This means it cannot bind to rvalue references. Hence, an overload would be selected that prefers lvalues (most likely an lvalue reference to const). We can cheat and simply cast the type to an rvalue reference:


 set_new_class(static_cast<MyClass&&>(my_class));


This works. This really says that we want the type system to pretend that what we’re passing it is an rvalue so it can bind to rvalue references. Enter the C++11 standard library. It’s not much of a help, but it does provide a standard function std::move which would do this cast for you:


 set_new_class(std::move(my_class));


Note that the last two lines of examples will do exactly the same thing. They simply inform the compiler that at the line of set_new_class, it can treat |my_class| as if it was an rvalue. Obviously, the code can still access |my_class| after the call, but (at least with most standard types), applying a move and passing it somewhere usually leaves it in an unspecified state. That is to say, you can either destroy the object or assign a new value to it; using an object in an unspecified state can lead to undefined behavior.


Note that it’s worth reiterating, std::move will not actually move anything or do anything tricks with memory. It will simply cast its parameter to be an rvalue reference, unconditionally. You can also use it with rvalues, of course


 set_new_class(std::move(MyClass()));  // silly, but works.


The reason this is silly, is because MyClass() is already an rvalue. In the best case, adding a move only adds noise. In the worst case, this can inhibit some compiler optimizations such as return value optimizations, which can result in slower code.


As an important aside, note that we have to still distinguish the value category of an expression from the variable type. That is, a variable can have a type of rvalue reference, but the variable itself remains an lvalue:


void function(MyClass&& my_class) {
 set_new_class(
std::move(my_class)); // std::move is required to make
}                         // |my_class| an rvalue. That is
                         // because |my_class| as an
                         // expression is an lvalue.


Here, std::move is not adding noise, it is taking |my_class|, an lvalue expression, and converting it to an rvalue expression. This is needed, because regardless of the variable’s type, it refers to an object, thus making it an lvalue.


Special case for return statements


There is one special case where normally you would be inclined to write std::move, but it’s not required: a return statement returning a local variable (or function parameter) of the same type as the function signature’s return type:


Foo function() {
 Foo foo;
 …
 return foo;
}


In this case, foo is still an lvalue at the return statement. However, the C++ standard provides a special exception in this case. It says that if Foo has a move constructor, then |foo| has to be treated as an rvalue in this case. However, if Foo doesn’t have a move constructor then it will be treated as an lvalue.


In short, you typically do not need to say std::move(foo) when returning a local foo.


Templates (std::forward)


Templates present a small complexity. Specifically, when you have a templated typename T, then reading T&& does not necessarily imply an rvalue reference:


template <typename T>
void function(T&& value);  // value might either be an lvalue
                          // reference or an rvalue
// reference!


The reason for this is simple: when passing an rvalue into this function, say 5, the type system determines that T could be int, which makes the function accept an int&& (an rvalue reference):


template <typename T>
void function(T&& value);


function(5);  // T = int, |value| type is int&&.


However, when passed an lvalue, the type system tries its best to bind the passed argument to value. It succeeds due to reference collapsing. Reference collapsing is a rule that protects us against references to references. In a nutshell, it says that an rvalue reference to an lvalue reference (ie Type& &&) collapses to be an lvalue reference (Type&). So, when presented with an lvalue int, the type system figures out that it could make T, the deduced template type, be an int&, thus making value have the type int& && which collapses to int&.


template <typename T>
void function(T&& value);


int five = 5;
function(five);  // T = int&, |value| type is int&.


This is important, because when encountering T&& in a template, it is crucial to understand that this could mean an rvalue reference or it could mean an lvalue reference. Consider the following example:


template <typename T>
void function(T&& value) {
  …
  set_new_value(std::move(value)); // Wrong.
}


This example is similar to above, except that it contains a template. Now, using move here is wrong, because the function could have been invoked with an lvalue and the caller expects the object to still be the same when the function returns:


Type type;
function(type);
// Do work with |type|, since it shouldn’t have been modified.


It would be very unexpected for function to move from the type when it is still needed. For our second attempt, we can rewrite function as follows:


template <typename T>
void function(T&& value) {
  …
  set_new_value(value); // OK, but can be inefficient.
// (also fails with move only types)
}


This variant is ok, since it works just fine with both lvalues and rvalues. However, when an rvalue is passed in and |value| has the type rvalue reference, it can be inefficient to pass |value| directly, since |value| itself is an lvalue expression and can cause a copy.


What is needed here is a conditional cast to an rvalue. The condition is based on whether the type of value is an rvalue reference or not. This is what the C++11 standard library std::forward does:


template <typename T>
void function(T&& value) {
  …
  set_new_value(std::forward<T>(value)); // Good.
}


What this will do is make |value| an rvalue if its type is an rvalue reference. Otherwise, it will leave |value| as an lvalue. This looks magical, but it works simply by inspecting the deduced type T. Note that for forward, you have to specify which specialization you would like, by explicitly saying std::forward<T>. Earlier we learned that if T is a non-reference type (int for example), then value is an rvalue reference. However, if T is an lvalue reference type (int& for example), then value is an lvalue reference:


Example code
T
|value| type
Explanation
function(5)
int
int&&
rvalue reference
function(five);
int&
int&
lvalue reference


This is exactly the same mechanism that allows forward to figure out whether to cast value to be an rvalue. In other words, when we see std::forward<T>, T is the part that causes us to do the right thing:


std::forward<int>(five);   // equivalent to “std::move(five)”
std::forward<int&>(five);  // equivalent to “five”


This is exactly what we want for a template. Given an unknown reference type (lvalue or rvalue reference), move it only if it was an rvalue reference. This process is known as perfect forwarding. In other words, forward only really applies to templated code, and it will act as move when the non-templated version would have used a move.


Given this, it is almost always wrong to see a call to std::forward in non-templated code. If the code isn’t templated, then it should be clear whether we want an lvalue or an rvalue. That is, we either leave the expression as is, or we apply std::move to it.


Pitfalls


It is clear that rvalue references are a powerful tool for writing efficient C++. However, there are also common pitfalls that one should watch out for:


  • Returning references to locals
  • Rvalue references to const
  • Pessimizing moves (applying std::move to expressions that are already rvalues)

No comments:

Post a Comment