std::forward is similar to std::move. It is just a static_cast to rvalue reference. But it is slightly different than std::move, std::forward is a conditional cast that only cast the type to rvalue-reference if some condition is satisify. Let dive deeper in it!
First of all, let look at a custom implementation of std::forward, this is not the exact definition in the STL, but it is close enough.
namespace std {
template<typename T>
__attribute__((always_inline))
constexpr T&& forward(std::remove_reference_t<T>& t) noexcept {
return static_cast<T&&>(t);
}
template<typename T>
__attribute__((always_inline))
constexpr T&& forward(std::remove_reference_t<T>&& t) noexcept {
static_assert(!std::is_lvalue_reference_v<T>);
return static_cast<T&&>(t);
}
};
the implementation might make absolutely no sense to you right now, but it will make sense soon.
Reference Collapsing
In C++, you are not allowed to explicitly write a reference to a reference (e.g., int& & will result in a compilation error). However, the compiler can generate references to references behind the scenes during template argument deduction, typedef/ using type aliases, decltype, and auto type deduction.
To handle these scenarios gracefully, C++11 introduce reference collapsing, a strict set of rules that simplifies a "reference to a reference" down to a single valid reference.
The easiest way to understand this is to think lvalue reference is infectious. If an lvalue reference is in anywhere in the combination, the result will collapse to a lvalue reference. The only way to get an rvalue reference (&&) is if both references are rvalue references.
Let look at the following table to understand what does it actually mean:
| Existing / Target Type | Applied Reference | Resulting Collapsed Type |
|---|---|---|
T& (lvalue ref) |
& (lvalue ref) |
T& |
T& (lvalue ref) |
&& (rvalue ref) |
T& |
T&& (rvalue ref) |
& (lvalue ref) |
T& |
T&& (rvalue ref) |
&& (rvalue ref) |
T&& |
Let's look at an example to understand what it means in practice:
using LRef = int&;
using RRef = int&&;
int x = 10;
LRef& r1 = x; // int& & collapses to int&
LRef&& r2 = x; // int& && collapses to int&
RRef& r3 = x; // int&& & collapses to int&
RRef&& r4 = 20; // int&& && collapses to int&&
This is a very important rule that make sure your template argument deduction work as expected!
std::forward
Now let us get back to the main topic of today: What is std::forward? Based on the reference collapsing rule, we can finally demystify the magic behind perfect forwarding.
The Problem: Named Rvalue References Are Actually Lvalues
Before we dissect std::forward, consider this classic forwarding dilemma:
void process(const std::string& s) { std::cout << "lvalue\n"; }
void process(std::string&& s) { std::cout << "rvalue\n"; }
template <typename T>
void wrapper(T&& arg) {
// What happens if we call process(arg)?
process(arg);
}
If we pass a temporary rvalue to wrapper:
wrapper(std::string("hello"));
You might expect process(std::string&&) to be called. But process(const std::string&) is called instead!
Why? Because even though arg might bind to an rvalue, the variable arg itself has a name.
Crucial Rule: In C++, if an expression has a name, it is an lvalue.
Inside wrapper, arg is an lvalue. If you pass it directly to another function, you will always pass an lvalue—losing the original temporary/moveable nature of the caller's argument.
Universal References (Forwarding References)
In the template signature template <typename T> void wrapper(T&& arg):
- If you pass an lvalue of type
Widget(e.g.,Widget w; wrapper(w);): - The compiler deduces
TasWidget&. The parameter type becomes
Widget& &&, which collapses toWidget&.If you pass an rvalue of type
Widget(e.g.,wrapper(Widget{});):The compiler deduces
TasWidget(or non-referenccepted as an lvalue reference, and forces the developer to explicitly provide template parameter T (disabling automatic template argument deduction).
The return type is a static cast tostatic_cast<T&&>(t).The parameter type remains
Widget&&.
Notice what happened: T encodes whether the argument was originally an lvalue (T = Widget&) or an rvalue (T = Widget)!
How std::forward<T> Uses Reference Collapsing
Now re-examine the primary implementation of std::forward:
template <typename T>
constexpr T&& forward(std::remove_reference_t<T>& t) noexcept {
return static_cast<T&&>(t);
}
Notice two critical details:
- The parameter is
std::remove_reference_t<T>& t. This ensurestis always accepted as an lvalue reference, and forces the developer to explicitly provide template parameterT(disabling automatic template argument deduction). - The return type is a static cast to
static_cast<T&&>(t).
Let’s trace the two cases when we write std::forward<T>(arg):
Case 1: Caller passed an Lvalue
-
Twas deduced asWidget&. - We call
std::forward<Widget&>(arg). - Inside
forward: - Return type is
T&&→Widget& &&, which collapses to `Widget&`. The cast evaluates to
static_cast<Widget&>(t).Result: An lvalue reference is returned. No move occurs.
Case 2: Caller passed an Rvalue
-
Twas deduced asWidget(non-reference). - We call
std::forward<Widget>(arg). - Inside
forward: - Return type is
T&&→Widget&&(no collapse needed). The cast evaluates to
static_cast<Widget&&>(t).Result: An rvalue reference is returned. The caller's temporary is restored as a moveable rvalue!
This is why people say std::forward is a conditional cast:
-
std::move<T>(x)unconditionally castsxtoT&&. -
std::forward<T>(x)only casts toT&&ifTis not an lvalue reference type.
What About the Second Overload?
Recall the second overload in the definition:
template <typename T>
constexpr T&& forward(std::remove_reference_t<T>&& t) noexcept {
static_assert(!std::is_lvalue_reference_v<T>,
"std::forward cannot convert an rvalue to an lvalue.");
return static_cast<T&&>(t);
}
Why is this needed?
This overload accepts an rvalue argument (&&). It exists primarily as a safety net:
-
Perfect forwarding of expressions / temporaries directly: e.g., forwarding return values like
std::forward<T>(make_widget()). - Preventing dangerous dangling references: If someone accidentally writes:
std::forward<Widget&>(Widget{});
They are trying to bind a temporary rvalue to an lvalue reference. The static_assert(!std::is_lvalue_reference_v<T>) triggers a clear compile-time error instead of silently binding to a destroyed temporary.
Putting It All Together
With std::forward, our wrapper function preserves value categories perfectly:
template <typename T>
void wrapper(T&& arg) {
// Restores original value category:
process(std::forward<T>(arg));
}
std::string name = "Alice";
wrapper(name); // Calls process(const std::string&) -> preserved as lvalue
wrapper(std::string("Bob")); // Calls process(std::string&&) -> preserved as rvalue
Key Takeaways
- Names mean lvalues: An rvalue reference parameter is itself an lvalue because it has an identifier.
-
Tremembers: Universal references capture the original category inside template parameterT. -
Collapsing makes it work:
static_cast<T&&>collapses to&whenT = U&, and remains&&whenT = U.
Top comments (0)