Just Software Solutions

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: , , ,
Stumble It! stumbleupon logo | Submit to Reddit reddit logo | Submit to DZone dzone logo

Comment on this post

If you liked this post, why not subscribe to the RSS feed RSS feed or Follow me on Twitter? You can also subscribe to this blog by email using the form on the left.

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) !.

by passingby at 15:00:33 on Monday, 21 January 2019

Thanks passingby. I've fixed a few typos, including those.

by Anthony Williams at 15:00:33 on Monday, 21 January 2019

Interesting caviat about the use of named rvalue-reference parameters inside non-template functions.. thanks!

by Hillel Y. Sims at 15:00:33 on Monday, 21 January 2019

There is a typo in the code:

std::vector<double>> data;

has an extra greater-than sign.

by Joshua Lehrer at 15:00:33 on Monday, 21 January 2019

Fixed typo. Thanks, Joshua.

by Anthony Williams at 15:00:33 on Monday, 21 January 2019

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; }

by passingby2 at 15:00:33 on Monday, 21 January 2019

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.

by Anthony Williams at 15:00:33 on Monday, 21 January 2019

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?

by passingby2 at 15:00:33 on Monday, 21 January 2019

@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

by Anthony Williams at 15:00:33 on Monday, 21 January 2019

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

by Josuel Guimaraes at 15:00:33 on Monday, 21 January 2019

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.

by Anthony Williams at 15:00:33 on Monday, 21 January 2019

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?

by andrew at 15:00:33 on Monday, 21 January 2019

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.

by Anthony Williams at 15:00:33 on Monday, 21 January 2019

Thanks for your complete answer!

by andrew at 15:00:33 on Monday, 21 January 2019

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 }

by Paul at 15:00:33 on Monday, 21 January 2019

Add your comment

Your name:

Email address:

Your comment:

Design and Content Copyright © 2005-2024 Just Software Solutions Ltd. All rights reserved. | Privacy Policy