Search code examples
c++c++11perfect-forwardingforwarding-reference

Aren't forwarding references deduced as r-value references?


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?


Solution

  • 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.