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.