4

I have a specific question regarding forwarding references. (I think) I understand r-value references and std::move, but I have trouble understanding forwarding references:

#include <iostream>
#include <utility>

template <typename T> class TD; // from "Effective Modern C++"

void consume(const int &i) { std::cout << "lvalue\n"; }
void consume(int &&i)      { std::cout << "rvalue\n"; }

template <typename T>
void foo(T&& x) {
//    TD<decltype(x)> xType; - prints int&&
    consume(x);
}

int main() {
    foo(1 + 2);
}

T is int, that's fine. If x is of type int&&, why it prints "lvalue" and we need std::forward ? I mean, where is the conversion from int&& to const int& here?

songyuanyao
  • 169,198
  • 16
  • 310
  • 405
0xF
  • 3,214
  • 1
  • 25
  • 29
  • 5
    `x` has a name, so it is a lvalue. – Jarod42 Mar 10 '21 at 15:13
  • 1
    lvalue / rvalue are _categories of expressions_. `decltype` unfortunately mixes expression categories and variable types :( – dyp Mar 10 '21 at 15:14
  • 2
    You might see the difference with `decltype((x))` versus `decltype(x)` [Demo](http://coliru.stacked-crooked.com/a/446648ba5649164a) – Jarod42 Mar 10 '21 at 15:16
  • 1
    related/dupe: https://stackoverflow.com/questions/26550603/why-should-i-use-stdforward and https://stackoverflow.com/questions/28483250/rvalue-reference-is-treated-as-an-lvalue – NathanOliver Mar 10 '21 at 15:17
  • The dupe is incorrect. I understand that (any) references can be converted to pointers with the address-of operator. That's not what I ask about. What I ask is why r-value references don't bind to r-value references, but to l-value references. – 0xF Mar 10 '21 at 15:52
  • Not sure how you think it doesn't answer the question. The second sentence in the accepted answer is *Whatever has a name is an lvalue*, which explains why the rvalue reference is being treated as an lvalue. – NathanOliver Mar 10 '21 at 16:07
  • My question is entirely different. I do not ask about the body of the `consume(int &&i)` function. While the answer you cited provides some insight, it doesn't explain the difference between types and value categories which seems to be the key here. – 0xF Mar 10 '21 at 16:22

2 Answers2

5

Types and value categories are two independent properties of expression.

Each C++ expression (an operator with its operands, a literal, a variable name, etc.) is characterized by two independent properties: a type and a value category. Each expression has some non-reference type, and each expression belongs to exactly one of the three primary value categories: prvalue, xvalue, and lvalue.

The type of x is int&&, but x is the name of variable and x is an lvalue expression itself, which just can't be bound to int&& (but could be bound to const int&).

(emphasis mine)

The following expressions are lvalue expressions:

  • the name of a variable, a function, a template parameter object (since C++20), or a data member, regardless of type, such as std::cin or std::endl. Even if the variable's type is rvalue reference, the expression consisting of its name is an lvalue expression;

That means when function has both lvalue-reference and rvalue-reference overloads, value categories are considered in overload resolution too.

More importantly, when a function has both rvalue reference and lvalue reference overloads, the rvalue reference overload binds to rvalues (including both prvalues and xvalues), while the lvalue reference overload binds to lvalues:

If any parameter has reference type, reference binding is accounted for at this step: if an rvalue argument corresponds to non-const lvalue reference parameter or an lvalue argument corresponds to rvalue reference parameter, the function is not viable.

std::forward is used for the conversion to rvalue or lvalue, consistent with the original value category of forwarding reference argument. When lvalue int is passed to foo, T is deduced as int&, then std::forward<T>(x) will be an lvalue expression; when rvalue int is passed to foo, T is deduced as int, then std::forward<T>(x) will be an rvalue expression. So std::forward could be used to reserve the value category of the original forwarding reference argument. As a contrast std::move always converts parameter to rvalue expression.

songyuanyao
  • 169,198
  • 16
  • 310
  • 405
  • Thanks! So the function parameter `x` has the _type_ of r-value reference, but any _use_ of `x` is an l-value expression because `x` is a function parameter? Which cannot initialize an r-value reference? This means types and value categories _aren't_ independent things. It is the value category of the argument to `consume` that affects selection of the overload, i.e. function with the correct parameter type. Can it be explained without understanding the five value categories and the long lists of rules about which expression is of which category? – 0xF Mar 10 '21 at 16:04
  • @0xF: I've memorized this as: "parameters of type `T&&` can be _constructed_ from an rvalue. But all named variables, including `T&&` variables, are then lvalues. – Mooing Duck Mar 10 '21 at 16:31
  • *"Types and value categories are two independent things."* I think this breaks when references come into play. E.g. `int& foo();` then the type determines that `foo()` is an lvalue :( – dyp Mar 10 '21 at 17:03
  • @0xF I tried to add some more explanations. – songyuanyao Mar 11 '21 at 01:03
  • Great answer! If someone reading this feels lost like I was yesterday, I recommend "50 shades of C++" on YouTube. I've just watched it now, only this time it was more scary and less funny than when I watched it back in 2019. – 0xF Mar 11 '21 at 15:50
4

The call consume(x) will always select the const lvalue reference overload of consume() because the expression x is an lvalue. This is regardless of the deduced type for x.

However, by calling consume() instead as:

consume(std::forward<T>(x));

It will select the rvalue reference overload if the value category of the argument passed to foo() (i.e., the wrapper function template with forwarding references) was an rvalue.


Roughly speaking, std::forward propagates or preserves the original value category of foo()'s argument to the argument of the nested call consume():

template <typename T>
void foo(T&& x) {
   // preserve original value category of foo()'s argument
   consume(std::forward<T>(x));
}

That is, if foo() is passed an rvalue, e.g.:

foo(1); // selects consume(int &&)

This argument's value category – 1, an rvalue – is further propagated through std::forward to the call to consume(), and therefore the rvalue reference overload (i.e., void consume(int &&)) is selected. If foo() is passed an lvalue instead, e.g.:

foo(i); // selects consume(const int&)

Since i is an lvalue, and std::forward preserves the original value category of this argument to the call to consume(), the lvalue reference overload is selected – i.e., void consume(const int&).

JFMR
  • 23,265
  • 4
  • 52
  • 76