简体   繁体   中英

C++ lvalues and rvalues in template functions

I took sample from http://www.cplusplus.com/reference/utility/forward :

// forward example
#include <utility>      // std::forward
#include <iostream>     // std::cout

// function with lvalue and rvalue reference overloads:
void overloaded (const int& x) {std::cout << "[lvalue]";}
void overloaded (int&& x) {std::cout << "[rvalue]";}

// function template taking rvalue reference to deduced type:
template <class T> void fn (T&& x) {
    overloaded (x);                   // always an lvalue
    overloaded (std::forward<T>(x));  // rvalue if argument is rvalue
}

int main () {
    int a;

    std::cout << "calling fn with lvalue: ";
    fn (a);
    std::cout << '\n';

    std::cout << "calling fn with rvalue: ";
    fn (0);
    std::cout << '\n';

    return 0;
}

which prints

calling fn with lvalue: [lvalue][lvalue]
calling fn with rvalue: [lvalue][rvalue]

But if I want to make overloaded template, like that:

template<typename T>
void overloaded (const T& x) {std::cout << "[lvalue]";}
template<typename T>
void overloaded (T&& x) {std::cout << "[rvalue]";}

it prints

calling fn with lvalue: [rvalue][rvalue]
calling fn with rvalue: [rvalue][rvalue]

Is there any way to use template function to deduct what an object I've transfered to function?

Yes it's possible, just add a const to your second overload:

template<typename T>
void overloaded (const T& x);
template<typename T>
void overloaded (const T&& x);
//               ^^^^^

The reason why you need to const is to make x not a forwarding reference. Forwarding references are very greedy, and if you don't pass in the exact same type (including any cv qualifiers) then the forwarding reference overload will get chosen.

In your case, because you do not pass a const object to overload , the second overload will always be a better match.

But if you add a const there, then it's not a forwarding reference anymore and can only accept rvalues and no lvalues, and won't be a better match for an lvalue as a result but will be a better match for any rvalues than the const& overload.


If you need to move from x , then you will have to do something else. Remove the const& overload and branch in the forwarding reference whether you have an rvalue or lvalue:

template <typename T> void overloaded(T &&x) {
  if /*constexpr*/ (std::is_lvalue_reference_v<T>)
    std::cout << "[lvalue]";
  else
    std::cout << "[rvalue]";
}

Note: You'll need to use if constexpr if you do specific stuff that is not valid for a branch or the other.

Whenever a function template parameter has type "rvalue reference to a type template parameter", like your x in template<typename T> void overloaded(T&& x); , it becomes a "forwarding reference" aka "universal reference". These follow the special rule that they can match either an lvalue or rvalue argument.

When the argument is an rvalue, T has its type as a non-reference, and the parameter type is an rvalue reference.

When the argument is an lvalue, T is an lvalue reference type. This makes sense because of a second "reference-collapsing rule": if you add a & or && to a type alias (a typedef , type defined with using , or type template parameter) which is already a reference, it forms a reference type which is an rvalue reference if both the alias is an rvalue reference and you add && , or an lvalue reference in the other three cases.

So when you call overloaded(x) when x is an lvalue of type int , both overloads of overloaded match:

template<typename T>
void overloaded (const T& x);
// Specialization void overloaded<int>(const int&);

template<typename T>
void overloaded (T&& x);
// Specialization void overloaded<int&>(int&);

But the second specialization is a better match for the argument type, so it wins.

To prevent this, I would use SFINAE techniques to restrict the second overloaded to only non-reference types.

#include <type_traits>

// As before:
template<typename T>
void overloaded (const T& x);

template<typename T>
auto overloaded (T&& x) -> std::enable_if_t<!std::is_reference<T>::value>;

Now given any lvalue argument, the second template will deduce T to be an lvalue reference type, fail to substitute that into the return type, and the template will then be ignored for overload resolution, so that the first overload can be used instead.

The second overloaded template isn't for rvalues, but rather it's a forwarding reference. It tends to be very aggressive about picking up call because it will deduce both const-enss and value-ness exactly correctly making it preferred in almost all cases (your int call is with an lvalue but it's not const, so that's why it gets called).

To make this work, you need to constrain that template. When that template matches an lvalue, T will be of reference type so that reference collapsing rules say that T&& will collapse to T& . So we can do this:

template<typename T>
void overloaded (const T& x) {std::cout << "[lvalue]";}

template<typename T, std::enable_if_t<!std::is_reference<T>::value, int> = 0>
void overloaded (T&& x) {std::cout << "[rvalue]";}

If you do this then you should get the expected output.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM