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
Stumble It! | Submit to Reddit | Submit to DZone
If you liked this post, why not subscribe to the RSS feed or Follow me on Twitter? You can also subscribe to this blog by email using the form on the left.
Design and Content Copyright © 2005-2024 Just Software Solutions Ltd. All rights reserved. | Privacy Policy
15 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.
Thanks for the good explanation.
Can you give some comments on the std::forward implementation of gcc?
It looks like this (move.h http://gcc.gnu.org/onlinedocs/libstdc++/latest-doxygen):
00047 // 20.2.2, forward/move 00048 template<typename _Tp> 00049 struct identity 00050 { 00051 typedef _Tp type; 00052 }; 00053 00054 template<typename _Tp> 00055 inline _Tp&& 00056 forward(typename std::identity<_Tp>::type&& __t) 00057 { return __t; }
The gcc implementation of std::forward is designed to avoid implicit conversions and automatic type deduction, since these actually prevent the function working as designed.
std::identity<X>::type is just a synonym for X, but it cannot be determined by auto deduction. Therefore you must explicitly specify the type to forward: std::forward<X>(an_x). If the specified type is a normal lvalue reference (std::forward<X&>(an_x)) then both the parameter and return type are the same lvalue reference type, since a reference-to-a-reference is just the original reference.
If the type being forwarded is a non-reference type, then the parameter and return type are rvalue references. This is therefore equivalent to static_cast<X&&>(an_x).
This implementation works as described in the article.
Thanks for your quick reply Anthony!
If I take this sentence from your post: "Within a function that takes its arguments as rvalue references, the named parameter is treated as an lvalue reference."
and combine it with a sentence from the reply you gave me earlier:
"If the type being forwarded is a non-reference type, then the parameter and return type are rvalue references. This is therefore equivalent to static_cast<X&&>(an_x)."
Taking these two together, I think for rvalue references the std::forward argument type is X& while the parameter type is X&&. But there is no implicit cast between these types, or is there?
Can you help me out here?
@passingby2 You've misread what I wrote. The named (rvalue reference) parameter is treated as an *lvalue reference*. The behaviour of std::forward for non-reference types is therefore irrelevant.
Anyway, to answer your question, an X& can bind to an X&& parameter. R-value references (X&&) can bind to lvalues and rvalues.
X x;
X& lref=x; //bind lvalue reference to object: fine
X&& rref=x; //bind rvalue reference to object: fine too.
X&& rref2=lref; // bind rvalue reference to lvalue ref: fine
X& lref2=X(); // bind lvalue reference to temporary (rvalue): error
X&& rref3=X(); // bind rvalue reference to temporary (rvalue): fine, extends lifetime of temporary
Hi, Anthony
First congratulation for your work with Boost.Thread...
Perfect your explanation! I'd like to know whether it aplies to class templates either,
such as:
template<class T> class my_queue : private std::deque<T> { ... void enqueue(T&& a) { ... push_back(std::forward<T>(a)); } ... };
Thanx in advance
Hi Josuel,
The auto-deduction mechanism only works for function templates, where the template parameter T is being deduced from the function call. In a member function of a class template, as in your example, T is specified by the class template specialization being used. Therefore your enqueue always takes an rvalue reference, but can also bind to an lvalue reference. In this instance you need to define both an rvalue reference overload *and* an lvalue reference overload if you wish to allow copying for lvalues and moving for rvalues.
Can you explain me the difference between forward and move? For example, `g(std::forward<T>(t));` and `g(std::move(t));`. When I must write the forward and when - the move?
Hi Andrew,
You use std::move(arg) whenever you want to move from arg. This unconditionally transfers the contents of arg to the target of the move, and in general you know nothing about the state of arg after the move --- though it is a valid object you no longer know what state it is in (e.g. a string may have any contents, a vector may have any number of elements, etc.). This is either an optimization (it avoids copying large objects unnecessarily), or essential for transferring ownership (e.g. with std::thread).
std::forward is designed for use in a template function which takes its arguments by T&&, where T is a template parameter. std::forward preserves the rvalue-ness of the arguments, so if your function was called with an rvalue then std::forward<T> provides an rvalue. If your function was called with an lvalue then std::forward<T> provides an lvalue. This works because of the way T is deduced from a T&& parameter: T is deduced to be "X&", for an lvalue argument of type X, whereas it is deduced to be plain "X" for an rvalue argument.
Thanks for your complete answer!
So do constructor initialisation lists still have a role to play?
Assuming a move-assignment is defined for an std::vector, could your X move constructor have been defined as follows. If I've made a mistake in this code, can we return to my first question :)
X(X&& other) // move constructor { data = std::move(other.data); // move the data: no copies }