Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Aren't forwarding references deduced as r-value references? [duplicate]

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?

like image 314
0xF Avatar asked Dec 30 '22 16:12

0xF


2 Answers

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.

like image 142
songyuanyao Avatar answered Jan 31 '23 12:01

songyuanyao


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

like image 44
ネロク・ゴ Avatar answered Jan 31 '23 14:01

ネロク・ゴ