How does std::forward work, especially when passing lvalue/rvalue references? [duplicate]
Asked Answered
H

3

174

Possible Duplicate:
What are the main purposes of std::forward and which problems does it solve?

I know what it does and when to use it but I still can't wrap my head around how it works. Please be as detailed as possible and explain when std::forward would be incorrect if it was allowed to use template argument deduction.

Part of my confusion is this: "If it has a name, it's an lvalue" - if that's the case why does std::forward behave differently when I pass thing&& x vs thing& x?

Hack answered 15/12, 2011 at 21:8 Comment(5)
Answered here. std::forward is really just syntactic sugar over static_cast<T&&>.Diabolic
The short answer why you cannot let the type be deduced: in the body of template <typename T> void foo(T && x);, the type of x is not the same as whatever T gets deduced as.Depository
The concepts that seems to be lacking is that type (for instance int) is not the same thing as "value category" (an int can be sometimes a lvalue if you use a variable int a, sometimes rvalue if you return it from a function int fun()). When you look at a parameter thing&& x its type is an rvalue reference, however, the variable named x also has a value category: it's an lvalue. std::forward<> will make sure to convert the "value category" x to match its type. It makes sure a thing& x is passed as a value category lvalue, and thing&& x passed as an rvalue.Dniren
Saying std::forward is equivalent to static_cast<T&&> is confusing because we are mixing the concepts of types and value category, which are two separate things.Dniren
At 34:06 Scott Meyers explains how type and value are two distinct things (Title: "C++ and Beyond 2012: Scott Meyers - Universal References in C++11")Dniren
P
203

First, let's take a look at what std::forward does according to the standard:

§20.2.3 [forward] p2

Returns: static_cast<T&&>(t)

(Where T is the explicitly specified template parameter and t is the passed argument.)

Now remember the reference collapsing rules:

TR   R

T&   &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&   && -> T&  // rvalue reference to cv TR -> TR (lvalue reference to T)
T&&  &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&&  && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)

(Shamelessly stolen from this answer.)

And then let's take a look at a class that wants to employ perfect forwarding:

template<class T>
struct some_struct{
  T _v;
  template<class U>
  some_struct(U&& v)
    : _v(static_cast<U&&>(v)) {} // perfect forwarding here
                                 // std::forward is just syntactic sugar for this
};

And now an example invocation:

int main(){
  some_struct<int> s1(5);
  // in ctor: '5' is rvalue (int&&), so 'U' is deduced as 'int', giving 'int&&'
  // ctor after deduction: 'some_struct(int&& v)' ('U' == 'int')
  // with rvalue reference 'v' bound to rvalue '5'
  // now we 'static_cast' 'v' to 'U&&', giving 'static_cast<int&&>(v)'
  // this just turns 'v' back into an rvalue
  // (named rvalue references, 'v' in this case, are lvalues)
  // huzzah, we forwarded an rvalue to the constructor of '_v'!

  // attention, real magic happens here
  int i = 5;
  some_struct<int> s2(i);
  // in ctor: 'i' is an lvalue ('int&'), so 'U' is deduced as 'int&', giving 'int& &&'
  // applying the reference collapsing rules yields 'int&' (& + && -> &)
  // ctor after deduction and collapsing: 'some_struct(int& v)' ('U' == 'int&')
  // with lvalue reference 'v' bound to lvalue 'i'
  // now we 'static_cast' 'v' to 'U&&', giving 'static_cast<int& &&>(v)'
  // after collapsing rules: 'static_cast<int&>(v)'
  // this is a no-op, 'v' is already 'int&'
  // huzzah, we forwarded an lvalue to the constructor of '_v'!
}

I hope this step-by-step answer helps you and others understand just how std::forward works.

Persistent answered 15/12, 2011 at 22:14 Comment(17)
"(Shamelessly stolen from this answer.)" Don't give it a second thought. They stole it from here: open-std.org/jtc1/sc22/wg21/docs/papers/2002/… :-)Potful
My real confusion was why std::forward isn't allowed to use template argument deduction, but I didn't want to ask it in those words because I already tried once before without results I could understand. I think I've figured it out now though (https://mcmap.net/q/120896/-why-is-template-argument-deduction-disabled-with-std-forward)Hack
I think the last code snippet won't work because you used a primitive type int which hasn't a move constructor. You should use something like std::vector<int> or string which has a move constructor.Suds
@Johnny: It works just fine. I kept the type simple (and short) because it's completely irrelevant and was just to show what happens with the deduction.Persistent
You're right there's no std::move around so there should be no problemsSuds
I guess I won't ever know but... why the downvote? Did I miss something? Did I say something wrong?Persistent
Another "shamelessly" source: msdn.microsoft.com/en-us/library/dd293668.aspx :*Judi
I'd give you a basket of flowers and a huge bar chocolate. Thank you!Last
Further discussed here: isocpp.org/blog/2012/11/… . This (wonderful!) answer parallels the "Nitty Gritty Details" section, but the first section really should be read for practical applications.Learned
Superb answer, nevertheless: To be ultimatively correct including argument deduction, the first line for both examples would read in ctor: '5' is rvalue (expression type 'int'), -> because rvalue no type transformation -> match 'int' and 'U' -> 'U' is deduced as 'int', giving 'int&&' and in ctor: 'i' is an lvalue (expression type 'int'), -> because lvalue add '&' -> match 'U' and 'int&' -> 'U' is deduced as 'int&', giving 'int& &&'Concernment
Howard Hinnant's N2951 suggests that implementing std::forward as a simple static_cast will produce runtime errors when std::forward is used carelessly. Does the standard mandate that implementations must use this simple static_cast method?Isleen
TR and R mean.....?Declassify
@Persistent Why does not deduce U as int&& in some_struct<int> s1(5)? You see int&& && equal to int&&.Palaeozoology
@Persistent Which statement is right when deducing int a;f(a):"since a is an lvalue, so int(T&&) equate to int(int& &&)" or "to make the T&& equate to int&, so T should be int&"? I prefer to the latter one.Palaeozoology
Can you help explain why i is not of type int, but int& in the final example?Opportunity
You could differentiate your wording in "named rvalue references, 'v' in this case, are lvalues" which is unprecise as the value category of v is still an rvalue (assert with decltype(v)) but the *expresion" that contains a named variable will evaluate to an lvalue IF you don't std::forward / static_cast<&&> it again.Batfowl
The example code still works without the static_cast. (Tested in www.onlinegdb.com). The int gets initialized with 5 in both cases. Seems perfect forwarding isn't actually required in this example.Channing
S
277

I think the explanation of std::forward as static_cast<T&&> is confusing. Our intuition for a cast is that it converts a type to some other type -- in this case it would be a conversion to an rvalue reference. It's not! So we are explaining one mysterious thing using another mysterious thing. This particular cast is defined by a table in Xeo's answer. But the question is: Why? So here's my understanding:

Suppose I want to pass you an std::vector<T> v that you're supposed to store in your data structure as data member _v. The naive (and safe) solution would be to always copy the vector into its final destination. So if you are doing this through an intermediary function (method), that function should be declared as taking a reference. (If you declare it as taking a vector by value, you'll be performing an additional totally unnecessary copy.)

void set(const std::vector<T> & v) { _v = v; }

This is all fine if you have an lvalue in your hand, but what about an rvalue? Suppose that the vector is the result of calling a function makeAndFillVector(). If you performed a direct assignment:

_v = makeAndFillVector();

the compiler would move the vector rather than copy it. But if you introduce an intermediary, set(), the information about the rvalue nature of your argument would be lost and a copy would be made.

set(makeAndFillVector()); // set will still make a copy

In order to avoid this copy, you need "perfect forwarding", which would result in optimal code every time. If you're given an lvalue, you want your function to treat it as an lvalue and make a copy. If you're given an rvalue, you want your function to treat it as an rvalue and move it.

Normally you would do it by overloading the function set() separately for lvalues and rvalues:

set(const std::vector<T> & lv) { _v = v; }
set(std::vector<T> && rv) { _v = std::move(rv); }

But now imagine that you're writing a template function that accepts T and calls set() with that T (don't worry about the fact that our set() is only defined for vectors). The trick is that you want this template to call the first version of set() when the template function is instantiated with an lvalue, and the second when it's initialized with an rvalue.

First of all, what should the signature of this function be? The answer is this:

template<class T>
void perfectSet(T && t);

Depending on how you call this template function, the type T will be somewhat magically deduced differently. If you call it with an lvalue:

std::vector<T> v;
perfectSet(v);

the vector v will be passed by reference. But if you call it with an rvalue:

perfectSet(makeAndFillVector());

the (anonymous) vector will be passed by rvalue reference. So the C++11 magic is purposefully set up in such a way as to preserve the rvalue nature of arguments if possible.

Now, inside perfectSet, you want to perfectly pass the argument to the correct overload of set(). This is where std::forward is necessary:

template<class T>
void perfectSet(T && t) {
    set(std::forward<T>(t));
}

Without std::forward the compiler would have to assume that we want to pass t by reference. To convince yourself that this is true, compare this code:

void perfectSet(T && t) {
    set(t);
    set(t); // t still unchanged
}

to this:

void perfectSet(T && t) {
    set(std::forward<T>(t));
    set(t); // t is now empty
}

If you don't explicitly forward t, the compiler has to defensively assume that you might be accessing t again and chose the lvalue reference version of set. But if you forward t, the compiler will preserve the rvalue-ness of it and the rvalue reference version of set() will be called. This version moves the contents of t, which means that the original becomes empty.

This answer turned out much longer than what I initially assumed ;-)

Sweeping answered 16/12, 2011 at 0:10 Comment(10)
void set(**const** std::vector & v) { _v = v; } Don't make this more complicated than it needs to be.Potful
"in this case it would be a conversion to an rvalue reference. It's not!" - Yes, it is! Inside of your perfectSet, t already is an lvalue. With the static_cast (or std::forward), we change it back to an rvalue.Persistent
@Xeo: Except when you call perfectSet with a reference to vector. As in: vector v; vector & vr; perfectSet(vr); When you're casting an lvalue reference to an rvalue reference, the result is still an lvalue reference. That's what I meant.Sweeping
@Bartosz: Even then you are not casting to an rvalue reference. As I said in my answer, you just cast to an lvalue reference, a no-op. The reference collapsing rules sort that out.Persistent
How do you format code in comments? The indentation trick didn't work.Sweeping
@Persistent I understand what you mean. Casting an lvalue reference to an rvalue reference results in an rvalue reference to an lvalue reference which, by the rules of reference collapsing, is the same as lvalue reference. I just don't think this explanation builds the right intuition.Sweeping
@Bartosz: The same way as inline code in an answer or question - backticks (`code here`)! - And no, the reference collapsing rules sort that out way before the cast, at the time the template gets instantiated to be exact. It realls is a cast from lvalue ref to lvalue ref.Persistent
let us continue this discussion in chatSweeping
Bartosz,why not make the set() to get forward references as parameter rather than two overloaded function?Abagael
@Bartosz Milewski Quote [emphasis mine]: In order to avoid this copy, you need "perfect forwarding", which would result in optimal code every time. If you're given an lvalue, you want your function to treat it as an lvalue and make a copy. If you're given an rvalue, you want your function to treat it as an rvalue and move it. How to comprehend it? If I understand what you mean correctly, it would invoke a copy constructor/operator if an lvalue is given whereas it would invoke a movement constructor/operator if an rvalue is given.Palaeozoology
P
203

First, let's take a look at what std::forward does according to the standard:

§20.2.3 [forward] p2

Returns: static_cast<T&&>(t)

(Where T is the explicitly specified template parameter and t is the passed argument.)

Now remember the reference collapsing rules:

TR   R

T&   &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&   && -> T&  // rvalue reference to cv TR -> TR (lvalue reference to T)
T&&  &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&&  && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)

(Shamelessly stolen from this answer.)

And then let's take a look at a class that wants to employ perfect forwarding:

template<class T>
struct some_struct{
  T _v;
  template<class U>
  some_struct(U&& v)
    : _v(static_cast<U&&>(v)) {} // perfect forwarding here
                                 // std::forward is just syntactic sugar for this
};

And now an example invocation:

int main(){
  some_struct<int> s1(5);
  // in ctor: '5' is rvalue (int&&), so 'U' is deduced as 'int', giving 'int&&'
  // ctor after deduction: 'some_struct(int&& v)' ('U' == 'int')
  // with rvalue reference 'v' bound to rvalue '5'
  // now we 'static_cast' 'v' to 'U&&', giving 'static_cast<int&&>(v)'
  // this just turns 'v' back into an rvalue
  // (named rvalue references, 'v' in this case, are lvalues)
  // huzzah, we forwarded an rvalue to the constructor of '_v'!

  // attention, real magic happens here
  int i = 5;
  some_struct<int> s2(i);
  // in ctor: 'i' is an lvalue ('int&'), so 'U' is deduced as 'int&', giving 'int& &&'
  // applying the reference collapsing rules yields 'int&' (& + && -> &)
  // ctor after deduction and collapsing: 'some_struct(int& v)' ('U' == 'int&')
  // with lvalue reference 'v' bound to lvalue 'i'
  // now we 'static_cast' 'v' to 'U&&', giving 'static_cast<int& &&>(v)'
  // after collapsing rules: 'static_cast<int&>(v)'
  // this is a no-op, 'v' is already 'int&'
  // huzzah, we forwarded an lvalue to the constructor of '_v'!
}

I hope this step-by-step answer helps you and others understand just how std::forward works.

Persistent answered 15/12, 2011 at 22:14 Comment(17)
"(Shamelessly stolen from this answer.)" Don't give it a second thought. They stole it from here: open-std.org/jtc1/sc22/wg21/docs/papers/2002/… :-)Potful
My real confusion was why std::forward isn't allowed to use template argument deduction, but I didn't want to ask it in those words because I already tried once before without results I could understand. I think I've figured it out now though (https://mcmap.net/q/120896/-why-is-template-argument-deduction-disabled-with-std-forward)Hack
I think the last code snippet won't work because you used a primitive type int which hasn't a move constructor. You should use something like std::vector<int> or string which has a move constructor.Suds
@Johnny: It works just fine. I kept the type simple (and short) because it's completely irrelevant and was just to show what happens with the deduction.Persistent
You're right there's no std::move around so there should be no problemsSuds
I guess I won't ever know but... why the downvote? Did I miss something? Did I say something wrong?Persistent
Another "shamelessly" source: msdn.microsoft.com/en-us/library/dd293668.aspx :*Judi
I'd give you a basket of flowers and a huge bar chocolate. Thank you!Last
Further discussed here: isocpp.org/blog/2012/11/… . This (wonderful!) answer parallels the "Nitty Gritty Details" section, but the first section really should be read for practical applications.Learned
Superb answer, nevertheless: To be ultimatively correct including argument deduction, the first line for both examples would read in ctor: '5' is rvalue (expression type 'int'), -> because rvalue no type transformation -> match 'int' and 'U' -> 'U' is deduced as 'int', giving 'int&&' and in ctor: 'i' is an lvalue (expression type 'int'), -> because lvalue add '&' -> match 'U' and 'int&' -> 'U' is deduced as 'int&', giving 'int& &&'Concernment
Howard Hinnant's N2951 suggests that implementing std::forward as a simple static_cast will produce runtime errors when std::forward is used carelessly. Does the standard mandate that implementations must use this simple static_cast method?Isleen
TR and R mean.....?Declassify
@Persistent Why does not deduce U as int&& in some_struct<int> s1(5)? You see int&& && equal to int&&.Palaeozoology
@Persistent Which statement is right when deducing int a;f(a):"since a is an lvalue, so int(T&&) equate to int(int& &&)" or "to make the T&& equate to int&, so T should be int&"? I prefer to the latter one.Palaeozoology
Can you help explain why i is not of type int, but int& in the final example?Opportunity
You could differentiate your wording in "named rvalue references, 'v' in this case, are lvalues" which is unprecise as the value category of v is still an rvalue (assert with decltype(v)) but the *expresion" that contains a named variable will evaluate to an lvalue IF you don't std::forward / static_cast<&&> it again.Batfowl
The example code still works without the static_cast. (Tested in www.onlinegdb.com). The int gets initialized with 5 in both cases. Seems perfect forwarding isn't actually required in this example.Channing
T
0

It works because when perfect forwarding is invoked, the type T is not the value type, it may also be a reference type.

For example:

template<typename T> void f(T&&);
int main() {
    std::string s;
    f(s); // T is std::string&
    const std::string s2;
    f(s2); // T is a const std::string&
}

As such, forward can simply look at the explicit type T to see what you really passed it. Of course, the exact implementation of doing this is non-trival, if I recall, but that's where the information is.

When you refer to a named rvalue reference, then that is indeed an lvalue. However, forward detects through the means above that it is actually an rvalue, and correctly returns an rvalue to be forwarded.

Tuckerbag answered 15/12, 2011 at 21:22 Comment(2)
Aha! Can you add some more examples (and what T is) for std::string &s, std::string&& s, const std::string&& s, std::string* s, std::string* const s?Hack
@Dave: No, not really. There are plenty of tutorials that go into reference collapsing more thoroughly.Tuckerbag

© 2022 - 2024 — McMap. All rights reserved.