Why an Rvalue Reference is Turned into Lvalue Reference by a Universal Reference [duplicate]
Asked Answered
I

4

4

I suppose when a universal reference parameter is matched with an rvalue reference argument, an rvalue reference argument is returned. However, my testing shows that the rvalue reference is turned into a lvalue reference by the universal reference function template. Why is it so?

#include <iostream>
#include <type_traits>
using namespace std;
template <typename T>
T f1(T&&t) {  //<-----this is a universal reference
  cout << "is_lvalue reference:" << is_lvalue_reference<T>::value << endl;
  cout << "is_rvalue reference:" << is_rvalue_reference<T>::value << endl;
  cout << "is_reference:"        << is_reference<T>::value        << endl;
  return t;
}
void f2(int&& t) {
  cout << "f2 is_lvalue reference:" << is_lvalue_reference<decltype(t)>::value << endl;
  cout << "f2 is_rvalue reference:" << is_rvalue_reference<decltype(t)>::value << endl;
  cout << "f2 is_reference:" << is_reference<decltype(t)>::value << endl;
  f1(t);

}

int main()
{
  f2(5);
  return 0;
}

In both GCC and VC++2010, this is the result:

f2 is_lvalue reference:0
f2 is_rvalue reference:1
f2 is_reference:1
is_lvalue reference:1
is_rvalue reference:0
is_reference:1

In other words, the parameter t in f2 was an rvalue reference, but when passed to f1, the parameter became a lvalue reference. Shouldn't it retain the rvalue-ness in f1?

Iranian answered 28/12, 2016 at 11:13 Comment(3)
No, it's the reason we need std::forwardPigskin
what are the specific rules related to universal reference? Scott Meyers, in his book, just said rvalue to rvalue reference, lvalue to lvalue referenceIranian
en.cppreference.com/w/cpp/language/value_categoryPigskin
A
9

The reason is that named rvalue references are treated as lvalues.

You should use std::move inside f2 when passing t to f1 to retain rvalueness:

void f2(int&& t) {
    f1(std::move(t));
}

Here you can find a good explanation.

Angary answered 28/12, 2016 at 11:18 Comment(3)
The link which you shared has an example at the very last Derived(Derived&& rhs) : Base(std::move(rhs)) // good, calls Base(Base&& rhs) { // Derived-specific stuff }. Now, suppose class Derived had data_member int x;. Please specify how rhs can now be used in assignment this->x=rhs.x. Since its a copy constructor so, Derived data needs to copied from the rhs to this.But since rhs is moved so, i guess it will not be valid to use it any more.More
ok, std::move() casts its argument to rvalue. But what is the type of the rvalue formed by this expression std::move(t)? rvalue of type int&& or rvalue of type int?Iranian
@Iranian rvalue of type int. Only expressions have value categories, and expressions cannot have reference types.Balneology
C
3

Calling f1(t), the argument is the expression t. Not static_cast<decltype(t)>(t) or something. Your examination of decltype(t) has nothing to do with the call of f1(t).

The expression t has type int and value category lvalue. (A rule of thumb is that if you can take the address of an expression then it is an lvalue, and you can certainly write &t). The "information" that a reference variable was originally declared as a reference is only visible via a decltype examination.

Since f1 is called with an lvalue, T is deduced to int&.

NB. You possibly want f1 to also use decltype(t) rather than T, if you ever want to see is_rvalue_reference being true in f1. For rvalue arguments, T deduces to a non-reference type, e.g. if you fix f2 by making it do f1(std::move(t)); then f1's T is int and decltype(t) in f1 is int&&.

Currin answered 28/12, 2016 at 13:14 Comment(0)
I
0

After studying the C++11 standard, I have a vague idea on what was going on after my f1(t); in f2. I describe it here to see if I got it right:

  1. In f2, t is an lvalue of type int&& (not int, this is an important difference)
  2. the call f1(t); causes type to be deduced like this:

    2.1 when T in f1 is given an lvalue, it is deduced as a reference of that lvalue's type orint&& &

    2.2 reference collapsing causes int&& & to become int &. This is the value of T.

  3. Since the parameter of f1 is declared as T&&, the type of parameter t in f1 is int & &&. So reference collapsing occurs a second time to deduce the type of t as int &.

  4. Hence T=int & and type of parameter t is int &. i.e. parameter t is an lvalue of type int &

Any comment?

Iranian answered 28/12, 2016 at 14:45 Comment(3)
There is no reference collapsing in step 2. The type of the variable t is int&&, while the type of the expression t is int. This is just how references work and have always worked, regardless of value category, even before rvalue references were added to the language.Murmansk
in that case, what is the value of T in step 2? Shouldn't it be int&& & instead of int &? If T is int&& &, isn't that we have to collapse it to get the parameter type?Iranian
T is not involved in step 2. f1 is called with an lvalue expression of type int, so in step 3, T is deduced as int&, so T&& is int& && which collapses to int&.Murmansk
N
-1

use the std::forward to keep t as rvalue.

void f2(int&& t) {
  //....
  f1(std::forward<int>(t));
}

The t argument is passed as rvalue and deduced as type int. (see how the deduction is done here )

std::forward(t) returns a rvalue of type int&& so that calls f1(int&&)

without std::forward(t), f1(t) takes the argument of type int and calls f1(int&)

UPDATE: Please be aware of the difference between

 is_lvalue_reference<T> and is_lvalue_reference<decltype<t>>. 

In the template, T is deduced based on the function argument, whereas the value category of t is always lvalue in f2 and

forward<int>(t) and move(t) 

are always rvalue.

#include <iostream>
#include <type_traits>

class A {};


template <typename T>
T f0(T& t) {  //<-----this is a universal reference
    std::cout<< "f0 lvalue"<<'\n';

    // take T as template argument, it is type int, so not rvalue, not lvalue.
//  std::cout << "is_lvalue reference:" << std::is_lvalue_reference<T>::value << std::endl;
    std::cout << "is_lvalue reference:" << std::is_lvalue_reference<T>::value << std::endl;
    std::cout << "is_rvalue reference:" << std::is_rvalue_reference<T>::value << std::endl;
    std::cout << "is_reference:"        << std::is_reference<T>::value        << std::endl;
    return t;
}

template <typename T>
T f0(T&&t) {  //<-----this is a universal reference
    std::cout<< "f0 rvalue"<<'\n';
    std::cout << "is_lvalue reference:" << std::is_lvalue_reference<T>::value << std::endl;
    std::cout << "is_rvalue reference:" << std::is_rvalue_reference<T>::value << std::endl;
    std::cout << "is_reference:"        << std::is_reference<T>::value        << std::endl;
    return t;
}

template <typename T>
T f1(T& t) {  //<-----this is a universal reference
    std::cout<< "f1 lvalue"<<'\n';

    // take T as template argument, it is type int, so not rvalue, not lvalue.
//  std::cout << "is_lvalue reference:" << std::is_lvalue_reference<T>::value << std::endl;
    std::cout << "is_lvalue reference:" << std::is_lvalue_reference<decltype(t)>::value << std::endl;
    std::cout << "is_rvalue reference:" << std::is_rvalue_reference<decltype(t)>::value << std::endl;
    std::cout << "is_reference:"        << std::is_reference<decltype(t)>::value        << std::endl;
    return t;
}

template <typename T>
T f1(T&&t) {  //<-----this is a universal reference
    std::cout<< "f1 rvalue"<<'\n';
    std::cout << "is_lvalue reference:" << std::is_lvalue_reference<decltype(t)>::value << std::endl;
    std::cout << "is_rvalue reference:" << std::is_rvalue_reference<decltype(t)>::value << std::endl;
    std::cout << "is_reference:"        << std::is_reference<decltype(t)>::value        << std::endl;
    return t;
}

void f2(int&&t) {  //<-----this is a universal reference
    std::cout<< "f2 rvalue"<<'\n';
    std::cout << "is_lvalue reference:" << std::is_lvalue_reference<decltype(t)>::value << std::endl;
    std::cout << "is_rvalue reference:" << std::is_rvalue_reference<decltype(t)>::value << std::endl;
    std::cout << "is_reference:"        << std::is_reference<decltype(t)>::value        << std::endl;


    f1(std::forward<int>(t));   // T is deduced as int for f(T&&), type int is not rvalue, nor lvalue, t is rvalue
    f1(std::move(t));           // T is deduced as int for f(T&&), type int is not rvalue, nor lvalue, t is rvalue
    f1(t);                      // T is deduced as int for f(T&),  type int is not rvalue, nor lvalue, t is lvalue
                                //if f1(T&) not exist, then f1(t) will call f1(T&&), T is deduced as int&, type int& is lvalue, t is lvalue


    f0(std::forward<int>(t));   // T is deduced as int for f(T&&), type int is not rvalue, nor lvalue, t is rvalue
    f0(std::move(t));           // T is deduced as int for f(T&&), type int is not rvalue, nor lvalue, t is rvalue
    f0(t);                      // T is deduced as int for f(T&),  type int is not rvalue, nor lvalue, t is lvalue
                                //if f0(T&) not exist, then f0(t) will call f0(T&&), T is deduced as int&, type int& is lvalue, t is lvalue
}




void test_rvalue()
{

    f2(5);
    std::cout << std::boolalpha;
    std::cout << std::is_lvalue_reference<A>::value << '\n';        // A is not lvalue
    std::cout << std::is_rvalue_reference<A>::value << '\n';        // A is not rvalue
    std::cout << std::is_lvalue_reference<A&>::value << '\n';       // A& is lvalue
    std::cout << std::is_rvalue_reference<A&&>::value << '\n';      // A&& is rvalue
    std::cout << std::is_lvalue_reference<int>::value << '\n';      // int is not lvalue
    std::cout << std::is_rvalue_reference<int>::value << '\n';      // int is not rvalue
    std::cout << std::is_lvalue_reference<int&>::value << '\n';     // int& is lvalue
    std::cout << std::is_rvalue_reference<int&&>::value << '\n';    // int&& is rvalue

}
Nowhere answered 17/1, 2021 at 12:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.