8

In a function template like this

template <typename T>
void foo(T&& x) {
  bar(std::forward<T>(x));
}

Isn't x an rvalue reference inside foo, if foo is called with an rvalue reference? If foo is called with an lvalue reference, the cast isn't necessary anyway, because x will also be an lvalue reference inside of foo. Also T will be deduced to the lvalue reference type, and so std::forward<T> won't change the type of x.

I conducted a test using boost::typeindex and I get exactly the same types with and without std::forward<T>.

#include <iostream>
#include <utility>

#include <boost/type_index.hpp>

using std::cout;
using std::endl;

template <typename T> struct __ { };

template <typename T> struct prt_type { };
template <typename T>
std::ostream& operator<<(std::ostream& os, prt_type<T>) {
  os << "\033[1;35m" << boost::typeindex::type_id<T>().pretty_name()
     << "\033[0m";
  return os;
}

template <typename T>
void foo(T&& x) {
  cout << prt_type<__<T>>{} << endl;
  cout << prt_type<__<decltype(x)>>{} << endl;
  cout << prt_type<__<decltype(std::forward<T>(x))>>{} << endl;
  cout << endl;
}

int main(int argc, char* argv[])
{
  foo(1);

  int i = 2;
  foo (i);

  const int j = 3;
  foo(j);

  foo(std::move(i));

  return 0;
}

The output of g++ -Wall test.cc && ./a.out with gcc 6.2.0 and boost 1.62.0 is

__<int>
__<int&&>
__<int&&>

__<int&>
__<int&>
__<int&>

__<int const&>
__<int const&>
__<int const&>

__<int>
__<int&&>
__<int&&>

Edit: I found this answer: https://stackoverflow.com/a/27409428/2640636 Apparently,

as soon as you give a name to the parameter it is an lvalue.

My question is then, why was this behavior chosen over keeping rvalue references as rvalues even when they are given names? It seems to me that the whole forwarding ordeal could be circumvented that way.

Edit2: I'm not asking about what std::forward does. I'm asking about why it's needed.

Community
  • 1
  • 1
SU3
  • 5,064
  • 3
  • 35
  • 66

4 Answers4

4

Isn't x an rvalue reference inside foo ?

No, x is a lvalue inside foo (it has a name and an address) of type rvalue reference. Combine that with reference collapsing rules and template type deduction rules and you'll see that you need std::forward to get the right reference type.

Basically, if what you pass to as x is a lvalue, say an int, then T is deduced as int&. Then int && & becomes int& (due to reference collapsing rules), i.e. a lvalue ref.

On the other hand, if you pass a rvalue, say 42, then T is deduced as int, so at the end you have an int&& as the type of x, i.e. a rvalue. Basically that's what std::forward does: casts to T&& the result, like a

static_cast<T&&>(x)

which becomes either T&& or T& due reference collapsing rules.

Its usefulness becomes obvious in generic code, where you may not know in advance whether you'll get a rvalue or lvalue. If you don't invoke std::forward and only do f(x), then x will always be a lvalue, so you'll be losing move semantics when needed and may end up with un-necessary copies etc.

Simple example where you can see the difference:

#include <iostream>

struct X
{
    X() = default;
    X(X&&) {std::cout << "Moving...\n";};
    X(const X&) {std::cout << "Copying...\n";}
};

template <typename T>
void f1(T&& x)
{
    g(std::forward<T>(x));
}

template <typename T>
void f2(T&& x)
{
    g(x);
}

template <typename T>
void g(T x)
{ }

int main()
{
    X x;
    std::cout << "with std::forward\n";
    f1(X{}); // moving

    std::cout << "without std::forward\n";
    f2(X{}); // copying
}

Live on Coliru

vsoftco
  • 55,410
  • 12
  • 139
  • 252
  • I understand what `std::forward`. I don't understand **why** it needs to do it. Just like you said, *if you pass a rvalue, say `int&&`, then `int&& &&` collapses to `int&&`*, but `int&&` is what we wan't, so, aren't we good to go? – SU3 Feb 18 '17 at 21:53
  • The problem is that in generic code you may not know in advance wether what you pass as `x` is a rvalue or lvalue. If you just say `f(x)` then `x` will always be considered a lvalue. – vsoftco Feb 18 '17 at 21:55
  • 1
    @SU3 `std::move` will always cast to rvalues, while `std::forward` will cast to rvalue only conditionally. So the `why` is because this cast to rvalue sometimes must be done. Also - remember that all function parameters are always lvalues - thats because they have a name. – marcinj Feb 18 '17 at 22:07
3

You really don't want your parameters to be automatically moved to the functions called. Consider this function:

template <typename T>
void foo(T&& x) {
  bar(x);
  baz(x);
  global::y = std::forward<T>(x);
}

Now you really don't want an automatic move to bar and an empty parameter to baz.

The current rules of requiring you to specify if and when to move or forward a parameter are not accidental.

Bo Persson
  • 90,663
  • 31
  • 146
  • 203
  • Do people really write this kind of code more often, where things happen to an rvalue reference in the same function before it's moved? I've only ever operated on an object after it's moved. So, this seems like an exceptional case to me. – SU3 Feb 18 '17 at 22:30
  • @vsoftco pointed out in a comment he has deleted that having things the other way around in the language would require an *anti_forward*. I think I'd prefer that. Are there any cases that that pattern could not handle? In your example, you could call `bar` and `baz` on the `global::y` afterwards instead. – SU3 Feb 18 '17 at 22:34
  • 2
    @SU3 If something has a non-zero probability of happening, it will happen for sure sometime in some program. And it will be a hard-to-spot bug. The committee probably thought (and I'm of the same opinion) that it's better to make a copy rather than to have an accidental move. `std::forward` is already a bit complicated, imagine having an anti-std::forward... people will go nuts :) – vsoftco Feb 18 '17 at 22:37
  • There is another case for this *forwarding reference* when `T` is actually some `const U&`, and `x` really isn't an rvalue at all. Using `std::forward(x)` will work for both cases. As I said in the previous comment, this has been *really thoroughly* considered before stating the rules for `T&&`. – Bo Persson Feb 18 '17 at 22:39
  • @BoPersson, I wasn't thinking that this wasn't *really thoroughly considered*. I just wanted to know what went into the consideration. Now, with your and vsoftco's help, I think I understand. – SU3 Feb 18 '17 at 22:42
1

I get exactly the same types with and without std::forward<T>

...no? Your own output proves you wrong:

__<int>    // T
__<int&&>  // decltype(x)
__<int&&>  // std::forward<T>(x)

Without using std::forward<T> or decltype(x) you will get int instead of int&&. This may inadvertently fail to "propagate the rvalueness" of x - consider this example:

void foo(int&)  { cout << "int&\n"; }
void foo(int&&) { cout << "int&&\n"; }

template <typename T>
void without_forward(T&& x)
{
    foo(x);
//      ^
//  `x` is an lvalue!
}

template <typename T>
void with_forward(T&& x)
{
//  `std::forward` casts `x` to `int&&`.
//      vvvvvvvvvvvvvvvvvv
    foo(std::forward<T>(x));
//                      ^
//          `x` is an lvalue!
}

template <typename T>
void with_decltype_cast(T&& x)
{
// `decltype(x)` is `int&&`. `x` is casted to `int&&`.
//      vvvvvvvvvvv
    foo(decltype(x)(x));
//                  ^
//          `x` is an lvalue!
}

int main()
{
    without_forward(1);    // prints "int&"
    with_forward(1);       // prints "int&&"
    with_decltype_cast(1); // prints "int&&"
}

wandbox example

Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416
  • I don't get it. How does my output prove me wrong? `int` is what I get for `T`. The type of `x` is not necessarily the same as `T`. What confuses me is that `decltype(x)` is `int&&`, but, as in your example, `foo(x)` selects the `foo(int&)` overload. Is `decltype(x)` not necessarily the type of `x` either? – SU3 Feb 18 '17 at 21:59
  • `decltype(x)` is the "declared type of `x`", which is `int&&`. Using `x` without `forward` or without any cast results in an *lvalue* expression, calling `foo(int&)`. – Vittorio Romeo Feb 18 '17 at 22:01
  • @SU3: updated my answer to show a `decltype(x)` cast example. – Vittorio Romeo Feb 18 '17 at 22:02
  • Also added inline comments - let me know if you still don't get it – Vittorio Romeo Feb 18 '17 at 22:04
  • Ok, I can take *"using `x` without any cast results in an lvalue`"* as an axiom. Then the behavior is as expected. But why was this the chosen design for the language. Would anything break if `x` maintained its status as an rvalue in such cases? – SU3 Feb 18 '17 at 22:05
  • @SU3: I don't have a good answer ready for that - I also think it requires its own separate question. – Vittorio Romeo Feb 18 '17 at 22:06
  • Right. I see now that I didn't formulate my question precisely enough, so the discussion went astray in the direction of "how things work" rather than "why they work this way". I intended to discuss the later from the get go. – SU3 Feb 18 '17 at 22:12
  • 2
    @SU3 Parameters are always considered to be lvalues (they are named and they have an address). Otherwise consider what will happen to situations like `f(x){ x++;}` when you invoke `f(42)`: you won't be able to modify `x` inside (cannot assign to a lvalue so `x++` won't compile). This is something most of us don't want in a programming language. Second, as @Bo Persson said, you don't want "accidental" moves, those are quite dangerous... imagine suddenly waking up in a different house, you won't want this, will you? (unless...) – vsoftco Feb 18 '17 at 22:32
  • @vsoftco I see. I thought that's how things were, that I'd make up in a different house if I'm not careful. – SU3 Feb 18 '17 at 22:38
  • @SU3 That happens in real life. In software, we have some control :) – vsoftco Feb 18 '17 at 22:39
0

x being an r-value is NOT the same thing as x having an r-value-reference type.

R-value is a property of an expression, whereas r-value-reference is a property of its type.

If you actually try to pass a variable that is an r-value reference to a function, it is treated like an l-value. The decltype is misleading you. Try it and see:

#include <iostream>
#include <typeinfo>
using namespace std;

template<class T> struct wrap { };

template<class T>
void bar(T &&value) { std::cout << " vs. " << typeid(wrap<T>).name() << std::endl; }

template<class T>
void foo(T &&value) { std::cout << typeid(wrap<T>).name(); return bar(value); }

int main()
{
    int i = 1;
    foo(static_cast<int &>(i));
    foo(static_cast<int const &>(i));
    foo(static_cast<int &&>(i));
    foo(static_cast<int const &&>(i));
}

Output:

4wrapIRiE   vs. 4wrapIRiE
4wrapIRKiE vs. 4wrapIRKiE
4wrapIiE     vs. 4wrapIRiE (these should match!)
4wrapIKiE   vs. 4wrapIRKiE (these should match!)

Community
  • 1
  • 1
user541686
  • 205,094
  • 128
  • 528
  • 886
  • 1
    Un-related: you can filter out with `./a.out | c++filt` (if using UNIX/Linux as it looks like you do) to get a nice (de-mangled) typename output. – vsoftco Feb 18 '17 at 22:42