6

I have this code, which doesn't compile, which is expected.

This is the error: an rvalue reference cannot be bound to an lvalue

class SomeData
{
public:
    vector<int> data;

    SomeData()
    {
        cout << "SomeData ctor" << endl;
        data.push_back(1);
        data.push_back(2);
        data.push_back(3);
    }

    SomeData(const SomeData &other)
    {
        cout << "SomeData copy ctor" << endl;
        data = other.data;
    }

    SomeData(SomeData &&other)
    {
        cout << "SomeData move ctor" << endl;
        data = move(other.data);
    }

    ~SomeData()
    {
        cout << "SomeData dtor" << endl;
    }

    void Print() const
    {
        for(int i : data)
            cout << i;

        cout << endl;
    }
};

void Function(SomeData &&someData)
{
    SomeData localData(someData);
    localData.Print();
}

int main(int argc, char *argv[])
{
    SomeData data;
    Function(data);                       // ERROR

    data.Print();

    return 0;
}

But, when I turn Function() into a template, it works fine, and uses the copy constructor of SomeData instead.

template<class T>
void Function(T &&someData)
{
    T localData(someData);                  // no more error
    localData.Print();
}


Is this standard C++ behaviour?

I've noticed that visual studio tends to be more forgiving when it comes to templates, so I am wondering if I can expect this same behaviour from all compliant C++11 compilers.

CuriousGeorge
  • 7,120
  • 6
  • 42
  • 74

3 Answers3

9

Yes. In the case of a template function, the compiler deduces the template argument T such that it matches the argument given.

Since someData is in fact an lvalue, T is deduced as SomeData &. The declaration of Function, after type deduction, then becomes

void Function(SomeData & &&)

and SomeData & &&, following the rules for reference collapsing, becomes SomeData &.

Hence, the function argument someData becomes an lvalue-reference and is passed as such to the initialization of localData. Note that (as @aschepler pointed out correctly) localData is declared as T, so it is itself a reference of type SomeData &. Hence, no copy construction happens here – just the initialization of a reference.


If you want localData to be an actual copy, you would have to turn the type from SomeData & into SomeData, i.e. you would have to remove the & from the type. You could do this using std::remove_reference:

template<class T>
void Function(T &&someData)
{
   /* Create a copy, rather than initialize a reference: */
    typename std::remove_reference<T>::type localData(someData);
    localData.Print();
}

(For this, you'd have to #include <type_traits>.)

Community
  • 1
  • 1
jogojapan
  • 68,383
  • 11
  • 101
  • 131
  • 2
    And this is in fact *expected and good* behaviour and the base for *perfect forwarding*. – Christian Rau Jul 17 '13 at 07:33
  • 1
    Is there a possible copy constructor, or is `localData` actually another reference to the same object? – aschepler Jul 17 '13 at 15:20
  • @aschepler is completely right. I blindly copied the claim about the copy constructor from the question, but of course, as `T` is deduced as `T &`, `localData` is itself just a reference. There is no copy performed here. – jogojapan Jul 17 '13 at 15:32
4

It is indeed intended behavior. Template functions of the form:

template< class T >
Ret Function( T&& param )
{
  ...
}

follows special rules (Ret can be or not a template, it doesn't matter). T&& is called a universal reference and it can basically bind to anything. This is because when template deduction kicks in and the param is in that form (beware, vector<T>&& is not a universal reference, neither is C<T> for any other template parameter), the reference collapsing rule is applied:

T = int& => T&& = int& && and that collapse to a single int&

the complete table of corrispondence is:

& + && = &
&& + & = &
&& + && = &&

So when you have the above function

int a = 5;
int b& = a;
Function( a ); // (1)
Function( b ); // (2)
Function( 3 ); // (3)

In case 1, T = int& and the deduced type is int& (since a is an lvalue) so Function has the following signature:

Ret Function( int& param ) // int& && collapses to int&

In case 2, T = int&

Ret Function( int& param ) // int& && collapses to int&

In case 3, T = int

Ret Function( int&& param ) 

This collapsing rule is what the committee found out to be reasonable to make perfect forwarding works. You can find the long story in this Scott Meyers's video

  • Thanks and +1 for providing extra details and examples. But case 1 isn't correct: `a` is indeed of type `int`, but it also is an lvalue, therefore `T` is deduced as `int & &&`, which collapses to `int &`. – jogojapan Jul 17 '13 at 12:51
  • Note that case (1) (@jogojapan) and case (3) are not correct: In case (1) `T` is deduced as `int&` and the resulting instantiated function is `Function(int&)` while in case (3) `T` is deduced as `int` and the resulting function is `Function(int&&)`. – MWid Jul 17 '13 at 12:56
  • @MWid Yes, that's right. And I just noticed my comment said `T` was deduced to `int & &&`, but of course MWid is correct: `T` is deduced to `int &`, and the function argument type is therefore `int & &&`, which collapses to `int &`. – jogojapan Jul 17 '13 at 12:59
2

Just to reassure you: this is not a MSVC problem, this is actually expected behaviour.

In templates, && has a different meaning when applied to template type parameters. It's called an universal reference.

I could probably paraphrase, but this article explains it very nicely so you should just read it.

To sum it up wildly (and very imprecisely), universal references have the ability to "decay" into ordinary references if the need arises.

syam
  • 14,701
  • 3
  • 41
  • 65
  • Paraphrasing it would be -- in the interest of Stackoverflow and future users -- really cool. – jogojapan Jul 17 '13 at 03:37
  • @jogojapan Well actually I still *did* paraphrase in my last paragraph after all, even though very succinctly. ;) I don't generally give link answers but in this case the topic is much bigger than it appears so for once the link seems warranted to me, it feels like a good addition to your answer. – syam Jul 17 '13 at 03:57
  • Yes, definitely. It's a useful answer, and a useful link. I didn't mean to imply otherwise. – jogojapan Jul 17 '13 at 04:42