Rvalue References and Perfect Forwarding in C++0x
Wednesday, 03 December 2008
One of the new features
in C++0x
is the rvalue reference. Whereas the a "normal" lvalue
reference is declared with a single ampersand &,
an rvalue reference is declared with two
ampersands: &&. The key difference is of course
that an rvalue reference can bind to an rvalue, whereas a
non-const lvalue reference cannot. This is primarily used
to support move semantics for expensive-to-copy objects:
class X
{
std::vector<double> data;
public:
X():
data(100000) // lots of data
{}
X(X const& other): // copy constructor
data(other.data) // duplicate all that data
{}
X(X&& other): // move constructor
data(std::move(other.data)) // move the data: no copies
{}
X& operator=(X const& other) // copy-assignment
{
data=other.data; // copy all the data
return *this;
}
X& operator=(X && other) // move-assignment
{
data=std::move(other.data); // move the data: no copies
return *this;
}
};
X make_x(); // build an X with some data
int main()
{
X x1;
X x2(x1); // copy
X x3(std::move(x1)); // move: x1 no longer has any data
x1=make_x(); // return value is an rvalue, so move rather than copy
}
Though move semantics are powerful, rvalue references offer more than that.
Perfect Forwarding
When you combine rvalue references with function templates you get an interesting interaction: if the type of a function parameter is an rvalue reference to a template type parameter then the type parameter is deduce to be an lvalue reference if an lvalue is passed, and a plain type otherwise. This sounds complicated, so lets look at an example:
template<typename T>
void f(T&& t);
int main()
{
X x;
f(x); // 1
f(X()); // 2
}
The function template f meets our criterion above, so
in the call f(x) at the line marked "1", the template
parameter T is deduced to be X&,
whereas in the line marked "2", the supplied parameter is an rvalue
(because it's a temporary), so T is deduced to
be X.
Why is this useful? Well, it means that a function template can
pass its arguments through to another function whilst retaining the
lvalue/rvalue nature of the function arguments by
using std::forward. This is called "perfect
forwarding", avoids excessive copying, and avoids the template
author having to write multiple overloads for lvalue and rvalue
references. Let's look at an example:
void g(X&& t); // A
void g(X& t); // B
template<typename T>
void f(T&& t)
{
g(std::forward<T>(t));
}
void h(X&& t)
{
g(t);
}
int main()
{
X x;
f(x); // 1
f(X()); // 2
h(x);
h(X()); // 3
}
This time our function f forwards its argument to a
function g which is overloaded for lvalue and rvalue
references to an X object. g will
therefore accept lvalues and rvalues alike, but overload resolution
will bind to a different function in each case.
At line "1", we pass a named X object
to f, so T is deduced to be an lvalue
reference: X&, as we saw above. When T
is an lvalue reference, std::forward<T> is a
no-op: it just returns its argument. We therefore call the overload
of g that takes an lvalue reference (line B).
At line "2", we pass a temporary to f,
so T is just plain X. In this
case, std::forward<T>(t) is equivalent
to static_cast<T&&>(t): it ensures that
the argument is forwarded as an rvalue reference. This means that
the overload of g that takes an rvalue reference is
selected (line A).
This is called perfect forwarding because the same
overload of g is selected as if the same argument was
supplied to g directly. It is essential for library
features such as std::function
and std::thread which pass arguments to another (user
supplied) function.
Note that this is unique to template functions: we can't do this
with a non-template function such as h, since we don't
know whether the supplied argument is an lvalue or an rvalue. Within
a function that takes its arguments as rvalue references, the named
parameter is treated as an lvalue reference. Consequently the call
to g(t) from h always calls the lvalue
overload. If we changed the call
to g(std::forward<X>(t)) then it would always
call the rvalue-reference overload. The only way to do this with
"normal" functions is to create two overloads: one for lvalues and
one for rvalues.
Now imagine that we remove the overload of g for
rvalue references (delete line A). Calling f with an
rvalue (line 2) will now fail to compile because you can't
call g with an rvalue. On the other hand, our call
to h with an rvalue (line 3) will still
compile however, since it always calls the lvalue-reference
overload of g. This can lead to interesting problems
if g stores the reference for later use.
Further Reading
For more information, I suggest reading the accepted rvalue reference paper and "A Brief Introduction to Rvalue References", as well as the current C++0x working draft.
Posted by Anthony Williams
[/ cplusplus /] permanent link
Tags: rvalue reference, cplusplus, C++0x, forwarding
Digg This | Save to del.icio.us | Stumble It! | Submit to Reddit | Submit to DZone
If you liked this post, why not subscribe to the RSS feed
?
5 Comments
"We therefore call the overload of g that takes an lvalue reference (line A)." and " This means that the overload of g that takes an rvalue reference is selected (line B)."
seem to contradict "Now imagine that we remove the overload of g for rvalue references (delete line A). "
I think there a two typos and first two sentences have A and B switched, otherwise, thanks for the excellent post (as usual) !.
Thanks passingby. I've fixed a few typos, including those.
Interesting caviat about the use of named rvalue-reference parameters inside non-template functions.. thanks!
There is a typo in the code:
std::vector<double>> data;
has an extra greater-than sign.
Fixed typo. Thanks, Joshua.