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.
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:
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.
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
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:
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.