Forwarding References, Reference Collapsing, and std::forward
Master template generic forwarding: universal (forwarding) references (T&&), reference collapsing rules (& + &&), and perfect argument forwarding via std::forward.
By the end, you’ll be able to…
- Distinguish between rvalue references (concrete T&&) and forwarding references (template T&&)
- Derive reference collapsing combinations (& + & = &, & + && = &, && + && = &&)
- Implement factory templates and emplace methods that forward arguments with exact value-category fidelity
01The Problem of Generic Argument Forwarding
When writing generic factory functions (like std::make_unique or std::make_shared) or container emplace methods (like std::vector::emplace_back), a function template must accept arbitrary arguments and forward them to a target constructor.
If the factory takes arguments by value (void make(T arg)), it incurs an unnecessary copy. If it takes non-const reference (void make(T& arg)), it cannot accept temporaries or literals like 42. If it takes const reference (void make(const T& arg)), it cannot move from rvalues, defeating move semantics!
To solve this combinatorial explosion, C++11 introduced Perfect Forwarding: the ability to forward arguments to another function preserving their exact original value category (whether they were lvalues, const lvalues, or rvalues).
02Universal (Forwarding) References vs Rvalue References
The token sequence '&&' has two completely different meanings depending on context:
1. Rvalue Reference: When '&&' is applied to a concrete, non-deduced type (e.g., Widget&& or std::string&&), it is strictly an rvalue reference that binds ONLY to rvalues.
2. Forwarding Reference (Universal Reference): When '&&' is applied to a deduced template type parameter (e.g., template <typename T> void func(T&& param)), it is a Forwarding Reference! It can bind to lvalues, const lvalues, and rvalues alike.
03The Four Reference Collapsing Rules
How does a template deduction handle references to references? In C++, you cannot declare a reference to a reference directly (e.g., int& & is illegal). However, when references arise during template type deduction, the compiler applies Reference Collapsing Rules:
1. & + & -> & (lvalue ref to lvalue ref collapses to lvalue ref)
2. & + && -> & (rvalue ref to lvalue ref collapses to lvalue ref)
3. && + & -> & (lvalue ref to rvalue ref collapses to lvalue ref)
4. && + && -> && (rvalue ref to rvalue ref collapses to rvalue ref)
Notice the fundamental rule: If either reference is an lvalue reference (&), the result collapses to an lvalue reference. An rvalue reference (&&) results ONLY when both references are rvalues!
04What std::forward Actually Does
Inside a function template taking T&& arg, the parameter arg itself has a name—which means arg is an lvalue! If you simply pass arg to another function, it will always be passed as an lvalue, losing its rvalue status.
std::forward<T>(arg) restores the original value category: it casts arg to an rvalue reference (T&&) if and only if the deduced template type T is not an lvalue reference. This ensures that temporaries are moved and lvalues are copied, achieving perfect forwarding.
+--------------------+--------------------+-----------------------+-------------------------+| Caller Passes | Deduced Type T | Deduced Param (T&&) | std::forward<T>(x) Yield|+--------------------+--------------------+-----------------------+-------------------------+| lvalue (int x) | int& | int& && -> int& | int& (lvalue reference) || const lvalue (cx) | const int& | const int& && -> c int&| const int& || rvalue (42 / move) | int | int&& | int&& (rvalue reference)|+--------------------+--------------------+-----------------------+-------------------------+The ideas worth keeping.
- Forwarding references (T&&) bind to both lvalues and rvalues by deducing reference types.
- Reference collapsing rules dictate that any combination containing an lvalue reference yields an lvalue reference.
- std::forward preserves original value categories for perfect argument forwarding into constructors.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Exercise 1: Write a template wrapper function named execu
Write a template wrapper function named execute_task(F&& func, Args&&... args) that perfectly forwards the callable and all arguments to std::invoke.
Explore a worked solution
#include <functional>#include <utility>template <typename F, typename... Args>decltype(auto) execute_task(F&& func, Args&&... args) { return std::invoke(std::forward<F>(func), std::forward<Args>(args)...);}Use std::forward<F>(func) and std::forward<Args>(args)... with parameter pack expansion.
Exercise 2: Explain why inside void func Widget w the
Explain why inside void func(Widget&& w), the expression 'w' is treated as an lvalue even though its type is Widget&&.
Explore a worked solution
// In C++, value category is a property of expressions, not types.// A named variable 'w' has identity: you can take its memory address using &w.// Because 'w' has identity and cannot be moved from automatically without explicit authorization,// the expression 'w' is an lvalue. To treat it as an rvalue again, you must call std::move(w).Does the expression have a name and can its address be taken?
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Exercise 1: Write a template wrapper function named execu
Write a template wrapper function named execute_task(F&& func, Args&&... args) that perfectly forwards the callable and all arguments to std::invoke.
Build completed. Binary executed. [Exit code: 0]
Keep exploring.
The official references behind this lesson, when you want a second angle or the full details.
Pause. Put the pieces together.
You’ve reached the end of Smart Pointers, Memory Ownership, and Perfect Forwarding. Before moving on, use this task to check what you can do independently.
Implement custom deleters and write generic factory functions utilizing std::forward.
Try it in your own environment and write down the result. This is a self-check; reading completion does not evaluate your code.
Review this moduleOne lesson closer.
Reading is a start. Trying it yourself makes the connection.