Right reference (rref) is a newly-proposed concept in C++11. Most containers in STL supports rref since then. In this article I’ll introduce and test the basic idea and usage of rref as well as related std::move and std::forward.

Right Reference

Right value is an expiring variable whose lifecycle is about to end is not addressable. Normally, most rvalues appear at the right side of = (where it gets named). A expiring variable can be referred by a rref as well as a const left reference. Since the value where constant left reference points will not (and can not) be changed, so lref can also refer to an expiring variable.

std::move does nothing but unconditionally converts the input value to a rref referring to it.

int x = 42;
int&& x_rref = std::move(x); // static_cast<T&&>(x);

std::move itself will not impact the performance since it only conduct a type conversion. But it can be applied in scenario where the the data will be transferred out from a variable which never being used later (like ownership transfer in Rust).

It’s worth emphasizing that the right reference itself is a left value since it’s a named variable and addressable:

int&& rref = 42;
// rref itself is a left value

It’s one of the most confusing concepts in C++11.

Universal Reference & Universal Collapse

We say a reference is universal when type inference occurs with right reference &&. In this case, this reference can refer to both left and right values.

template <typename T> void func_wrapper(T&& param)

Note universal reference only accompanies with type inference, otherwise only right reference is considered.

Since T&& param can receive both right and left values, and T& param can also receive left and right values (recap that a right reference itself is a left value). Thus there are 4 combinations for the parameter and argument:

Parameter (formal)Argument (real)Inferred type T
T&T&lvalue
T&T&&lvalue
T&&T&lvalue
T&&T&&rvalue

The template type T will be inferred as rvalue only when parameter and argument are both declared as right reference.

Perfect Forwarding

template <typename T> void func(T& param) { std::cout << "Left " << param << std::endl; }
 
template <typename T> void func(T&& param) { std::cout << "Right " << param << std::endl; }

Suppose we have two overloaded functions now (yes, functions could be overloaded with rvalue and lvalue parameter). And we use a wrapper to call these two:

template <typename T> void func_wrapper(T&& param) { func(param); }

What will happen if we pass different types of arguments? The answer is, param will fall to lvalue in all conditions (even if we call with rvalues like func_wrapper(42)). param is interpreted as lvalue since it is named variable and can be addressed. Thus all calls to func flow to the T& version.

The solution is using std::forward<T>, it keeps the information of argument to avoid aforementioned rvalue failure. std::forward<T> returns lvalue only when T is initialized as lvalue. So when we call func_wrapper with rvalues, std::forward can help us find the right version of overloaded functions.

Noting again perfect forwarding also happens with universal reference, so it works with template parameter or auto. Any determined type like int&& is right reference.

Summary

Use std::move when passing to rref, use std::forward when passing to universal ref.