I came across an another very clarifying talk of the legend Scott Meyers presenting Universal References in C++11 back in 2012. In this talk he calls our attention to the misleading usage of template parameters T&&, which we usually assume being rvalue references in all situations. It happens that it is not always the case.

The fact is that T&& becomes a rvalue reference or a lvalue reference depending on the case. Meyers defines a universal reference as a variable or parameter that fulfills the following requirements

  1. It is declared as T&&;
  2. T is a deduced type;

If a universal reference is initialized with a lvalue, then it becomes a lvalue reference. If a universal reference is initialized with a rvalue, then it becomes a rvalue reference.

Let’s consider a template function f

template<typename T>
void f(T&& param);

and an instance of some arbitrary class

SomeClass c;

Then T&& will be interpreted by the compiler differently depending on the usage of f:

f(c);               // instantiated as f(SomeClass&);
f(std::move(c));    // instantiated as f(SomeClass&&);
f(SomeClass());     // instantiated as f(SomeClass&&);

As you may notice, we get an lvalue reference in the first case.

const T&& is not a universal reference.

The same logic is applied to the use of auto declarations:

auto&& v = 10;      // type is int&&

std::vector<int> v;
auto&& e = v[5];    // type is int&

Attention: In the case of a template class (such as std::vector<T>) with method parameters using its templates (such as std::vector<T>::push_back(const T&) and std::vector<T>::push_back(T&&)), the type is resolved at class instantiation and therefore there is no universal reference – there is no type deduction. std::vector<T>::emplace_back<...Args>(Args&&...) on the other hand gets Args resolved by deduction, therefore a universal reference.

The importance of distinguishing between lvalue and rvalue instantiations along with template references gets more clear when we do things like move constructors or function overloads:

class MyClass {
    template<typename T>
    void doStuff(const T& param);   // takes const lvalues only
    template<typename T>
    void doStuff(T&& param);        // takes everything else!
};

If we don’t pay attention, the code might actually run one function instead of the function we might be mistakenly be expecting it to. Take some time to think about it: non-const lvalue are sent to the second function, and not the first.

Rules of Thumb

Speaking of lvalues and rvalues, here are some tips to keep in mind:

void f(const SomeClass& param) {
    // Use param normally
}
void f(SomeClass&& param) {
    // Access param with std::move(param)
    // Although SomeCLass&& is a rvalue reference type, param is a named variable,
    // therefore a lvalue, leading to copy operations. So we must make sure to work 
    // with param as its original rvalue semantic
}
template<typename T>
void f(T&& param) {
    // Access param with std::forward<T>(param)
    // Since param can virtually be anything we want to keep its original 
    // lvalue/rvalue semantic
}

Under the hood

For the curious out there, this transformation between rvalue references to lvalue references caused by universal references happens because the way the compiler deduces reference types. For example,

template<typename T>
void f(T&& param);

SomeClass c;
f(c);

f will be instantiated with its template parameter T deduced as SomeClass&:

void f<SomeClass&>(SomeClass& && param);

However, reference of reference is not allowed, so the compiler resolves it by collapsing references following the rules

T& &        // collapses to T&
T&& &       // collapses to T&
T& &&       // collapses to T&
T&& &&      // collapses to T&&

Then the final version for our function becomes

void f<SomeClass&>(SomeCLass& param);

auto follows the exact same rules.