Forwarding Reference vs Rvalue Reference in C++
In modern C++, both forwarding references (historically termed
"universal references") and standard rvalue references share the exact
same && token syntax, which frequently causes
confusion. This article breaks down the syntactic and contextual
differences between the two, explaining how type deduction and the
presence of const determine whether an expression acts as a
strict rvalue reference or as a flexible forwarding reference capable of
binding to both lvalues and rvalues.
The Underlying Syntactic Identity
Both references use the double ampersand (&&).
The compiler decides whether && represents an
rvalue reference or a forwarding reference based entirely on the
presence of type deduction.
- Rvalue Reference: Used when the type is fully known and fixed (no type deduction occurs).
- Forwarding Reference: Used when the type being
referenced is explicitly deduced by the compiler during function
template instantiation or
autotype inference.
Syntax of an Rvalue Reference
An rvalue reference binds strictly to rvalues (temporary objects and
values explicitly cast via std::move). Its syntax requires
a concrete, non-deduced type:
void process(int&& x); // Concrete type: rvalue reference
void process(std::string&& s); // Concrete type: rvalue referenceEven inside a template class, an argument using
&& might still be a normal rvalue reference if its
type does not depend on a deduction taking place at the function call
site:
template <typename T>
class Widget {
// T is deduced when the class is instantiated, NOT when push is called.
// Therefore, this is an rvalue reference, NOT a forwarding reference.
void push(T&& item);
};Syntax of a Forwarding Reference
A forwarding reference must involve a type that is being deduced at the point of initialization or call. Syntactically, it appears in two primary forms:
1. Function Templates with Template Parameter Deduction
template <typename T>
void process(T&& arg); // T is deduced here: forwarding referenceBecause T is deduced when calling
process(val), passing an lvalue causes T to
deduce as an lvalue reference (int&), while passing an
rvalue causes T to deduce as a non-reference type
(int).
2. Auto Type Deduction
auto&& var = getValue(); // Forwarding referenceLike template deduction, auto&& inspects the
initializer expression. If getValue() returns an lvalue,
var becomes an lvalue reference; if it returns a temporary
rvalue, var becomes an rvalue reference.
Critical Rules Altering the Syntax
Small syntactic adjustments strip away forwarding capabilities, turning the reference back into a standard rvalue reference:
The
constQualifier: Addingconstdisables forwarding behavior because a forwarding reference must be able to bind to a mutable lvalue without forcingconstupon it.template <typename T> void process(const T&& arg); // Rvalue reference to const, NOT a forwarding reference.Container Modifiers: Wrapping the deduced type inside another type eliminates forwarding properties:
template <typename T> void process(std::vector<T>&& vec); // Rvalue reference to a vector, NOT a forwarding reference.
Reference Collapsing: Why the Distinction Matters
The syntax dictates whether the C++ reference collapsing rules are triggered:
&+&\(\rightarrow\)&&+&&\(\rightarrow\)&&&+&\(\rightarrow\)&&&+&&\(\rightarrow\)&&
Forwarding reference syntax relies on these rules to preserve the
value category of the argument. Consequently, standard convention
mandates using std::move on rvalue references (to transfer
ownership) and std::forward<T> on forwarding
references (to preserve whether the caller passed an lvalue or an
rvalue).