7

I've understood how std::move works and implemented my own version for practice only. Now I'm trying to understand how std::forward works:

I've implemented this so far:

#include <iostream>


template <typename T>
T&& forward_(T&& x)
{
    return static_cast<T&&>(x);
}


/*template <typename T>
T&& forward_(T& x)
{
    return static_cast<T&&>(x);
}*/

void incr(int& i)
{
    ++i;
}

void incr2(int x)
{
    ++x;
}

void incr3(int&& x)
{
    ++x;
}

template <typename T, typename F>
void call(T&& a, F func)
{
    func(forward_<T>(a));
}


int main()
{

    int i = 10;
    std::cout << i << '\n';
    call(i, incr);
    std::cout << i << '\n';

    call(i, incr2);
    std::cout << i << '\n';

    call(0, incr3); // Error: cannot bind rvalue reference of type int&& to lvalue of type int.

    std::cout << "\ndone!\n";
}

Why must I provide the overloaded forward(T&) version taking an lvalue reference? As I understand it a forwarding reference can yield an lvalue or an rvalue depending on the type of its argument. So passing the prvalue literal 0 to call along with the incr3 function that takes an rvalue reference of type int&& normally doesn't need forward<T>(T&)?!

  • If I un-comment the forward_(T&) version it works fine!?

  • I'm still confused about: why if I only use the forward_(T&) version does it work for any value category? Then what is the point in having the one taking a forwarding reference forward_(T&&)?

  • If I un-comment the version taking lvalue reference to T& and the one taking forwarding reference T&& then the code works fine and I've added some messages inside both to check which one called. the result is the the one with T&& never called!

      template <typename T>
      T&& forward_(T& x)
      {
          std::cout << "forward_(T&)\n";
          return static_cast<T&&>(x);
      }
    
      template <typename T>
      T&& forward_(T&& x)
      {
          std::cout << "forward_(T&&)\n";
          return static_cast<T&&>(x);
      }
    
  • I mean running the same code in the driver program I've shown above.

Maestro
  • 2,512
  • 9
  • 24
  • [MCVE](http://coliru.stacked-crooked.com/a/8a4499bf2d4e2fa6) – Asteroids With Wings Dec 01 '20 at 20:50
  • 3
    You don't need an overloaded version... the `forward(T&)` is the only version that you should have. "As I guess a forwarding reference can yield an lvalue or an rvalue depending the type of its argument." Nope, the kind of reference depends on its template argument not its argument. – Ben Voigt Dec 01 '20 at 20:58
  • @BenVoigt: Yes it works if I use the version taking an lvalue reference to `T` only but why then the standard added the version taking forwarding reference? `T&&`? – Maestro Dec 01 '20 at 21:03
  • @Maestro See https://stackoverflow.com/q/27501400/580083. TL;DR: it forces you to provide an explicit template argument for `std::forward`. Live demo: https://godbolt.org/z/ed8GKb – Daniel Langr Dec 01 '20 at 21:06

2 Answers2

5

A T&& reference stops being a forwarding reference if you manually specify T (instead of letting the compiler deduce it). If the T is not an lvalue reference, then T&& is an rvalue reference and won't accept lvalues.

For example, if you do forward_<int>(...), then the parameter is an rvalue reference and ... can only be an rvalue.

But if you do forward_(...), then the parameter is a forwarding reference and ... can have any value category. (Calling it like this makes no sense though, since forward_(x) will have the same value category as x itself.)

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • Please explain more I'm so confused about it. – Maestro Dec 01 '20 at 20:54
  • @Maestro Edited. Is it better now? – HolyBlackCat Dec 01 '20 at 20:56
  • 1
    I've edited my `call` function to call `func(forward_(a));, then func(forward_(a));` but still the same error?! – Maestro Dec 01 '20 at 20:58
  • 1
    @Maestro Yes, because doing `forward_(...)` without specifying the template argument is completely useless (the result has the same value category as the argument). – HolyBlackCat Dec 01 '20 at 20:59
  • I've also used: `forward_(a)` but still the same. – Maestro Dec 01 '20 at 21:00
  • @Maestro Yes, in this case it's no different from `forward_(a)`. I'm not sure how to explain it, because I don't know what you don't understand. – HolyBlackCat Dec 01 '20 at 21:04
  • @Maestro Do you understand that if you do `int x; forward_(x);`, `T` gets deduced as `int&` and why that happens? – HolyBlackCat Dec 01 '20 at 21:05
  • I think because `x` is an lvalue but the problem is if I do `call(forward_(0), incr3);` doesn't work too! – Maestro Dec 01 '20 at 21:08
  • I'm still confused about: why if I use the version of `forward_(T&)` only works for any value category? then what is the point in having the one taking a forwarding reference `forward_(T&&)`? I'm so sorry for bothering you. – Maestro Dec 01 '20 at 21:11
  • 3
    @Maestro: The basic problem is that *inside* of `call(T&& a, F func)`, **`a` is always an *lvalue***. Even if the parameter to `call()` arrived as an rvalue, once it comes inside and is given the name `a`, it is an lvalue. `forward(a)` is putting the category from lvalue back to the category it arrived with. `a` is always an lvalue when it reaches `forward()`. – Ben Voigt Dec 01 '20 at 21:55
  • @BenVoigt: Thank you. That is what I guess so only the version `forward(T&)` then what is the point in having `forward(T&&)`? – Maestro Dec 01 '20 at 22:00
  • 1
    @Maestro: [Daniel already answered that one](https://stackoverflow.com/questions/65098198/why-doesnt-my-forward-function-work-for-rvalues/65098363?noredirect=1#comment115088659_65098198) when you asked in a comment on the question. – Ben Voigt Dec 01 '20 at 22:06
1

It is clear that you wander why having two versions of std::forward; one takes an l-value reference to the type parameter T& and the other takes a universal reference (forwarding) to the type parameter. T&&.

In your case you are using forward_ from inside the function template call which has forwarding reference too. The problem is that even that function call called with an rvalue it always uses forward_ for an lvalue because there's no way that call can pass its arguments without an object (parameter). Remember that a name of an object is an lvlaue even if it's initialized from an r-value. That is why always in your example forward_(T&) is called.

  • Now you ask why there's second version taking forwarding reference? It is so simple and as you may have already guessed: it is used for r-values (the values not the names of those objects).

Here is an example:

template <typename T>
T&& forward_(T& x)
{
    std::cout << "forward_(T&)\n";
    return static_cast<T&&>(x);
}

template <typename T>
T&& forward_(T&& x)
{
    std::cout << "forward_(T&&)\n";
    return static_cast<T&&>(x);
}

int main()
{

    int i = 10;
    forward_(i); // forward(T&)  (1)
    forward_(5); // forward(T&&) (2)
    forward_("Hi"); // forward(T&) (3)
}
Raindrop7
  • 3,889
  • 3
  • 16
  • 27