If you’re like me, you’ve been writing C++03 for a long long
time, and only recently you’ve gotten the chance to finally dive deep into C++11 and C++14.
There are many, many new features and concepts in C++11 and C++14. Some, like lambdas, are very easy to comprehend and apply based on knowledge from other languages, like JavaScript. Others, like rvalue references and perfect forwarding, well, not so much. This post will attempt to explain rvalue references and perfect forwarding from the perspective of a C++03 programmer just now coming into the C++11 world.
So, what’s an rvalue reference?
An rvalue reference is a reference that binds to an rvalue, like a temporary object. Note that lvalue references to const
(e.g. const Foo& foo = bar + baz;
) can also bind to rvalues, but rvalue references allow you to modify the referenced object. You can’t do that with lvalue references to const
!
// Reference to const std::string.
const std::string& fooLvalueRefConst = bar + baz; // OK
fooLvalueRefConst[0] = 'f'; // Error! foo is a reference to const std::string!
// Rvalue reference to std::string.
std::string&& fooRvalueRef = bar + baz; // OK
fooRvalueRef[0] = 'f'; // OK
Cool. But why do that?
To avoid making an unnecessary copy! Consider the common scenario of using a reference to const
in a constructor to initialize a member:
class Foo
{
public:
std::string member;
Foo(const std::string& member): member{member} {}
};
// Later on...
Foo foo{bar + baz};
What happens? bar + baz
creates a temporary std::string
, the const std::string& member
parameter binds to that temporary, and then that temporary is copied to Foo::member
.
By using an rvalue reference, we can skip copying the temporary by moving it directly into the member:
class Foo
{
public:
std::string member;
Foo(std::string&& member): member{std::move(member)} {}
};
// Later on...
Foo foo{bar + baz};
Now, bar + baz
creates a temporary, the std::string&& member
parameter binds to that temporary, and we invoke Foo::member
’s move constructor with member{std::move(member)}
.
Note that the member
parameter itself is not an rvalue, it’s an lvalue of type rvalue reference. In order to invoke std::string
’s move constructor, not its copy constructor, we use std::move(member)
to cast the member
parameter back to an rvalue.
That last part is very important. Rvalue references mark binding sites (e.g. Foo&& foo
mean that foo
can bind to a temporary object), but the references themselves are lvalues (foo
would not bind to another rvalue reference without std::move()
).
Awesome! But what if I don’t have a temporary object, so I do want a copy?
Aha! Your caught the problem with the last example. We can’t do this:
std::string bar{"baz"};
Foo foo{bar}; // Error! bar is not an rvalue!
So what do we have to do to allow construction with both lvalues and rvalues? One option is to provide two constructors:
class Foo
{
public:
std::string member;
// Copy member.
Foo(const std::string& member): member{member} {}
// Move member.
Foo(std::string&& member): member{std::move(member)} {}
};
However, imagine if we had two members. Or three members. Or four members. The number of constructors we would need would grow exponentially!
Here’s a better option:
class Foo
{
public:
std::string member;
Foo(std::string member): member{std::move(member)} {}
};
When used with an rvalue, member
’s move constructor would be invoked, and then Foo::member
’s move constructor would be invoked. Zero copies, two moves. Not bad!
When used with an lvalue, member
’s copy constructor would be invoked, and then Foo::member
’s move constructor would be invoked. One copy, one move. Great!
More importantly, this option grows linearly with the number of parameters!
Great, are we done? What was that thing about perfect forwarding?
Almost there! As we saw above, we end up with either two moves, or a move and a copy. Unfortunately, we could also end up with two copies for parameters/members that don’t have move constructors. In fact, this is why C++03 code relies so heavily on lvalue references to const
, to avoid unnecessary copies.
How do we fix it? With perfect forwarding! Perfect forwarding allows us to write one function (or constructor), just like the example above, and perfectly forward each parameter either as an rvalue or as an lvalue, depending on how it was passed in.
In other words:
// Forwards temporary as an rvalue into Foo::member.
// Zero copies, one move.
Foo foo{bar + baz};
// Forwards bar as an lvalue into Foo::member.
// One copy, zero moves.
Foo foo2{bar};
Here’s how to write the new constructor:
class Foo
{
public:
std::string member;
template<typename T>
Foo(T&& member): member{std::forward<T>(member)} {}
};
This works through template type deduction and reference collapsing.
Passing in an lvalue results in T = std::string&
(or T = const std::string&
for a const
lvalue). An rvalue reference to an lvalue reference collapses into an lvalue reference (std::string& &&
turns into std::string&
), giving us std::string& member
as the parameter. Then, std::forward<std::string&>(member)
returns member
unchanged (as an lvalue reference), and we trigger Foo::member
’s copy constructor by passing in an std::string&
.
Passing in an rvalue results in T = std::string
, giving us std::string&& member
as the parameter. std::forward<std::string>(member)
turns member
from an lvalue (remember, rvalue references are themselves lvalues) back into an rvalue, and we trigger Foo::member
’s move constructor by passing in an std::string&&
.
That’s it! Clever use of templates and a helper to cast back into the desired type of value (lvalue if given an lvalue, rvalue if given an rvalue). Note that this is similar to std::move()
, but std::move()
always casts to an rvalue, whereas std::forward<T>()
depends on the template type.
Finally, to wrap everything up, here’s an example with two parameters and template type checking (and excessive commenting):
class Foo
{
public:
std::string member;
std::string member2;
template<
typename T, // Parameter 1.
typename U, // Parameter 2.
// Template type checking.
typename = typename std::enable_if<
// Check type of parameter 1.
std::is_constructible<std::string, T>::value &&
// Check type of parameter 2.
std::is_constructible<std::string, U>::value>::type>
Foo(T&& member, U&& member2):
member{std::forward<T>(member)},
member2{std::forward<U>(member2)}
{}
};