Universal references

The Partridge Family were neither partridges nor a family. Discuss.
Post Reply
albinopapa
Posts: 4289
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Universal references

Post by albinopapa » May 31st, 2020, 10:45 am

I've known about universal references for awhile, but never really understand how they work until recently. A universal reference can be either an lvalue reference or an rvalue reference and they only exist in the context of type deduction and that's the part I recently found out or rather got clarification on. Consider the following:

Code: Select all

// Here, F() requires an lvalue reference.  It's type is known and we have a single &
void F( int& );

// Here, F() requires an rvalue reference.  It's type is known and we have a double &&
void F( int&& );

// Here, F() can take either an lvalue or rvalue reference.  It's type is unknown and we have a double &&
template<typename T> void F( T&& );

// So what about this F()?
template<typename T>
class A
{
public:
    void F( T&& );
};
You'd probably think universal reference and that's what I thought, but this is actually an rvalue reference. The reason is because the T is deduced for the class and not the function. This means that the compiler doesn't have to deduce the T for F(), it just needs to substitute whatever T is after deducing it for the class. So, A<int> means the compiler can just create an A<int>::F( int&& ) function instead of A<T>::F<T>( T&& ) function.

In case you're wondering, a universal reference or forwarding reference is whatever type of reference is passed to it.

Using the previous universal reference example above:

int num = 42;
F( num ); <-- named variable is lvalue so F( T&& ) is actually: F( int& );
F( 69 ); <-- unnamed variable is rvalue so F( T&& ) is actually: F( int&& );
F( A<int>{} ) <-- still unnamed, F( T&& ) is actually F( A<int>&& );
A<int> a;
F( std::move( a ) ) <-- a is returned from function as rvalue so F( T&& ) is F( A<int>&& );

This allows developers to write a single template function and the compiler will be able to pass either a lvalue or rvalue reference instead of programmer having to write two separate functions.

This also explains why std::vector and other containers that have a push or push_back function have one for lvalue ( copy ) and rvalue ( move ), it's because std::vector<> is templated on a type and push/push_back piggy backs off that T.

Gah, what a waste though. I was working on a Signal/Slot class ( similar to chili's Dispatch class from the Discord channel ), and was having trouble passing in callback functions. The reason was due to the template arguments were for the class and not the functions that store my function objects.
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

Post Reply