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, usestd::forward
when passing to universal ref.