Scott Meyers' Universal References
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
- It is declared as
T&&
; 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 asstd::vector<T>::push_back(const T&)
andstd::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 getsArgs
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.