Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Preventing non-const lvalues from resolving to rvalue reference instead of const lvalue reference

I'm having trouble overloading a function to take a value either by const reference or, if it is an rvalue, an rvalue reference. The problem is that my non-const lvalues are binding to the rvalue version of the function. I'm doing this in VC2010.

#include <iostream>
#include <vector>

using namespace std;

template <class T>
void foo(const T& t)
{cout << "void foo(const T&)" << endl;}

template <class T>
void foo(T&& t)
{cout << "void foo(T&&)" << endl;}

int main()
{
    vector<int> x;
    foo(x); // void foo(T&&) ?????
    foo(vector<int>()); // void foo(T&&)
}

The priority seems to be to deduce foo(x) as

foo< vector<int> & >(vector<int>& && t)

instead of

foo< vector<int> >(const vector<int>& t)

I tried replacing the rvalue-reference version with

void foo(typename remove_reference<T>::type&& t)

but this only had the effect of causing everything to resolve to the const-lvalue reference version.

How do I prevent this behaviour? And why is this the default anyway - it seems so dangerous given that rvalue-references are allowed to be modified, this leaves me with an unexpectedly modified local variable.

EDIT: Just added non-template versions of the functions, and they work as expected. Making the function a template changes the overload resolution rules? That is .. really frustrating!

void bar(const vector<int>& t)
{cout << "void bar(const vector<int>&)" << endl;}

void bar(vector<int>&& t)
{cout << "void bar(vector<int>&&)" << endl;}

bar(x); // void bar(const vector<int>&)
bar(vector<int>()); // void bar(vector<int>&&)
like image 535
Ayjay Avatar asked Oct 13 '11 00:10

Ayjay


2 Answers

When you have a templated function like this you almost never want to overload. The T&& parameter is a catch anything parameter. And you can use it to get any behavior you want out of one overload.

#include <iostream>
#include <vector>

using namespace std;

template <class T>
void display()
{
    typedef typename remove_reference<T>::type Tr;
    typedef typename remove_cv<Tr>::type Trcv;
    if (is_const<Tr>::value)
        cout << "const ";
    if (is_volatile<Tr>::value)
        cout << "volatile ";
    std::cout << typeid(Trcv).name();
    if (is_lvalue_reference<T>::value)
        std::cout << '&';
    else if (is_rvalue_reference<T>::value)
        std::cout << "&&";
    std::cout << '\n';
}

template <class T>
void foo(T&& t)
{
    display<T>();
}

int main()
{
    vector<int> x;
    vector<int> const cx;
    foo(x); // vector<int>&
    foo(vector<int>()); // vector<int>
    foo(cx);  // const vector<int>&
}
like image 184
Howard Hinnant Avatar answered Nov 10 '22 06:11

Howard Hinnant


In order for T&& to bind to an lvalue reference, T must itself be an lvalue reference type. You can prohibit the template from being instantiated with a reference type T:

template <typename T>
typename std::enable_if<!std::is_reference<T>::value>::type foo(T&& t)
{
    cout << "void foo(T&&)" << endl;
}

enable_if is found in <utility>; is_reference is found in <type_traits>.

The reason that the overload taking T&& is preferred over the overload taking a T const& is that T&& is an exact match (with T = vector<int>&) but T const& requires a qualification conversion (const-qualification must be added).

This only happens with templates. If you have a nontemplate function that takes a std::vector<int>&&, you will only be able to call that function with an rvalue argument. When you have a template that takes a T&&, you should not think of it as "an rvalue reference parameter;" it is a "universal reference parameter" (Scott Meyers used similar language, I believe). It can accept anything.

Allowing a T&& parameter of a function template to bind to any category of argument is what enables perfect forwarding.

like image 39
James McNellis Avatar answered Nov 10 '22 06:11

James McNellis