Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 901
J. S. Avatar asked Dec 11 '22 06:12

J. S.


2 Answers

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.

like image 50
Rakete1111 Avatar answered Jan 07 '23 12:01

Rakete1111


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.

like image 24
aschepler Avatar answered Jan 07 '23 13:01

aschepler