Perfect Forwarding — Forwarding References, std::forward, Reference Collapsing, Variadic Forwarding
In this tutorial, you will learn about Perfect Forwarding. We cover key concepts, practical examples, and best practices to help you master this topic.
C++ perfect forwarding uses forwarding references (T&&) and std::forward to pass function arguments through wrapper functions while preserving their exact value category, const-ness, and reference-ness.
What You'll Learn
You will understand forwarding references (T&& in template context), apply std::forward to preserve value categories, implement variadic forwarding wrappers for Factory functions, use reference collapsing rules (T& + && → T&), and build generic delegates and functional wrappers.
Why It Matters
Without perfect forwarding, wrapper functions like make_unique, emplace_back, and std::bind would require overloads for every combination of lvalue and rvalue parameters. Perfect forwarding eliminates this explosion. Every C++ generic library uses it — mastering forwarding is essential for writing generic code that works with both copies and moves.
Learning Path
graph LR
A["52: Move Semantics"] --> B["53: Perfect Forwarding"]
B --> C["54: Structured Bindings"]
C --> D["55: if/switch init + if constexpr"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
The Problem: Why Forwarding is Needed
Without forwarding, wrappers force copies or lose value categories.
#include <iostream>
#include <string>
#include <utility>
class Heavy {
std::string data_;
public:
Heavy(const std::string& s) : data_(s) {
std::cout << "Constructed\n";
}
Heavy(const Heavy&) { std::cout << "Copied\n"; }
Heavy(Heavy&&) noexcept { std::cout << "Moved\n"; }
};
// Bad wrapper: always copies
template <typename T>
Heavy badMakeHeavy(T arg) {
return Heavy(arg); // Always a copy of arg
}
// Better but wrong: always rvalue reference
template <typename T>
Heavy rvalueOnlyMakeHeavy(T&& arg) {
// arg is an lvalue (it has a name)
return Heavy(arg); // Still copies! arg is an lvalue
}
int main() {
std::string s = "very long string that would be expensive to copy";
auto h1 = badMakeHeavy(s); // Copies s
auto h2 = badMakeHeavy(std::move(s)); // Copies s (move is ignored)
// s is in unspecified state after move
}
Forwarding References
A T&& parameter in a template context is a forwarding reference (not an rvalue reference). It can bind to both lvalues and rvalues.
#include <iostream>
#include <string>
#include <utility>
// T&& is a forwarding reference because T is deduced
template <typename T>
void showCategory(T&& arg) {
if constexpr (std::is_lvalue_reference_v<T>) {
std::cout << "Lvalue (T = " << typeid(T).name() << ")\n";
} else {
std::cout << "Rvalue\n";
}
}
int main() {
std::string s = "hello";
showCategory(s); // Lvalue (T = std::string&)
showCategory(std::move(s)); // Rvalue (T = std::string)
showCategory("temporary"); // Rvalue (T = const char (&)[10])
// How it works:
// When arg is an lvalue: T = T& → T&& = T& (reference collapsing)
// When arg is an rvalue: T = T → T&& = T&&
}
Reference Collapsing Rules
C++ has four combinations of reference-to-reference:
| Original | Collapsed | Rule |
|---|---|---|
T& & |
T& |
Two lvalues → lvalue |
T& && |
T& |
Lvalue + rvalue → lvalue |
T&& & |
T& |
Rvalue + lvalue → lvalue |
T&& && |
T&& |
Two rvalues → rvalue |
Rule: If either is &, the result is &. Only T&& && collapses to &&.
#include <iostream>
#include <type_traits>
int main() {
// Reference collapsing in action
using LRef = int&;
using RRef = int&&;
// These are the collapsed types
using A = LRef&; // int&
using B = LRef&&; // int&
using C = RRef&; // int&
using D = RRef&&; // int&&
std::cout << "A is int&: " << std::is_same_v<A, int&> << "\n"; // true
std::cout << "B is int&: " << std::is_same_v<B, int&> << "\n"; // true
std::cout << "C is int&: " << std::is_same_v<C, int&> << "\n"; // true
std::cout << "D is int&&: " << std::is_same_v<D, int&&> << "\n"; // true
}
std::forward — Conditional Move
std::forward<T>(arg) casts arg back to the value category it originally had.
#include <iostream>
#include <string>
#include <utility>
class Heavy {
public:
Heavy(const std::string& s) { std::cout << "Constructed from lvalue\n"; }
Heavy(std::string&& s) { std::cout << "Constructed from rvalue\n"; }
};
// Correct wrapper with perfect forwarding
template <typename T>
Heavy makeHeavy(T&& arg) {
return Heavy(std::forward<T>(arg));
// If arg was an lvalue: std::forward<T>(arg) → static_cast<T&>(arg) → lvalue
// If arg was an rvalue: std::forward<T>(arg) → static_cast<T&&>(arg) → rvalue
}
int main() {
std::string s = "test";
auto h1 = makeHeavy(s); // "Constructed from lvalue"
auto h2 = makeHeavy(std::move(s)); // "Constructed from rvalue"
auto h3 = makeHeavy("temporary"); // "Constructed from rvalue"
}
Variadic Perfect Forwarding
The most powerful use: forwarding any number of arguments with any types.
#include <iostream>
#include <memory>
#include <string>
#include <utility>
// Factory function that forwards all arguments
template <typename T, typename... Args>
std::unique_ptr<T> makeUnique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
class Person {
std::string name_;
int age_;
public:
Person(std::string name, int age)
: name_(std::move(name)), age_(age) {
std::cout << "Person(" << name_ << ", " << age_ << ")\n";
}
};
int main() {
// Forward two arguments: string (rvalue) and int (prvalue)
auto p = makeUnique<Person>("Alice", 30);
// Forward via an lvalue
std::string name = "Bob";
auto p2 = makeUnique<Person>(name, 25); // name is forwarded as lvalue
// std::make_unique works exactly like this
auto p3 = std::make_unique<Person>("Charlie", 35);
}
The pack expansion std::forward<Args>(args)... applies std::forward to each argument individually, preserving each one's value category.
Real-World: emplace_back Implementation
std::vector::emplace_back uses perfect forwarding to construct elements in place.
#include <iostream>
#include <vector>
#include <string>
#include <utility>
// Simplified emplace_back
template <typename T>
class SimpleVector {
T* data_;
size_t size_;
size_t capacity_;
public:
template <typename... Args>
void emplace_back(Args&&... args) {
if (size_ == capacity_) {
// Reallocate (simplified — real implementation handles more)
reserve(capacity_ * 2 + 1);
}
// Construct in place: forward args to T's constructor
new (data_ + size_) T(std::forward<Args>(args)...);
++size_;
}
};
int main() {
std::vector<std::string> vec;
// emplace_back forwards "hello" to string's constructor
vec.emplace_back(5, 'h'); // Constructs "hhhhh" in place
vec.emplace_back("hello"); // Constructs "hello" in place
for (const auto& s : vec) {
std::cout << s << " ";
}
std::cout << "\n"; // hhhhh hello
// Without perfect forwarding, emplace_back couldn't forward arguments
// It would need to copy pre-constructed objects (like push_back)
}
Forwarding Lambdas (C++20)
C++20 lambdas with explicit template parameters enable perfect forwarding in lambdas.
#include <iostream>
#include <memory>
#include <utility>
int main() {
// C++14 generic lambda (does not forward perfectly)
auto bad = [](auto&& x) {
// x is lvalue (has name)
// return someFunc(x); // Always lvalue
// return someFunc(std::forward<decltype(x)>(x)); // Correct
};
// C++20 forwarding lambda with explicit template parameter
auto forwarder = []<typename T>(T&& x) {
return std::forward<T>(x);
};
// Practical: make_unique wrapper as lambda
auto make_unique_lambda = []<typename T, typename... Args>(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
};
auto p = make_unique_lambda.operator()<std::pair<int, double>>(42, 3.14);
std::cout << p->first << " " << p->second << "\n"; // 42 3.14
}
Forwarding for Getter/Setter
Perfect forwarding enables efficient setters that work with both lvalues and rvalues.
#include <iostream>
#include <string>
#include <utility>
class Widget {
std::string name_;
public:
// Perfect forwarding setter
void setName(auto&& name) {
name_ = std::forward<decltype(name)>(name);
// If called with lvalue: copies
// If called with rvalue: moves
}
// Traditional approach: two overloads
// void setName(const std::string& name) { name_ = name; } // copy
// void setName(std::string&& name) { name_ = std::move(name); } // move
const std::string& getName() const { return name_; }
};
int main() {
Widget w;
std::string s = "hello";
w.setName(s); // Copies s
std::cout << s << "\n"; // "hello" (still valid)
w.setName(std::move(s)); // Moves from s
std::cout << s << "\n"; // "" (moved-from)
w.setName("temporary"); // Moves from temporary
}
Common Mistakes
Mistake 1: Using std::move instead of std::forward in forwarding functions
template <typename T>
void wrapper(T&& arg) {
// Wrong: forces move even for lvalue inputs
target(std::move(arg));
// Correct: preserves value category
target(std::forward<T>(arg));
}
Mistake 2: Forwarding the same argument twice
template <typename T>
void bad(T&& arg) {
func1(std::forward<T>(arg));
func2(std::forward<T>(arg)); // If arg was rvalue, it's been moved!
}
Mistake 3: Forgetting that named variables are lvalues
template <typename T>
void wrapper(T&& arg) {
target(arg); // Always lvalue! Need std::forward
}
Mistake 4: Applying std::forward to non-forwarding references
void func(int&& arg) {
// arg is an rvalue reference, but it's a named lvalue
target(std::forward<int>(arg)); // OK but unusual
target(std::move(arg)); // More conventional for non-template
}
Mistake 5: Not using forwarding in variadic templates
template <typename... Args>
void bad(Args... args) { // By value: always copies
target(args...);
}
template <typename... Args>
void good(Args&&... args) { // Forwarding references
target(std::forward<Args>(args)...);
}
Practice Questions
What does
std::forwarddo? Answer: It conditionally casts its argument to an rvalue reference, restoring the original value category that the template parameter deduced.What is reference collapsing? Answer: The rule that determines the actual reference type when multiple references are nested.
T& &&collapses toT&, whileT&& &&collapses toT&&.Why is
T&&a forwarding reference in templates but an rvalue reference in non-templates? Answer: In templates,Tcan be deduced asU&, triggering reference collapsing. In non-templates,T&&is always an rvalue reference.What is the output?
template <typename T>
void f(T&&) { std::cout << "f called\n"; }
int main() {
int x = 5;
f(x);
f(5);
}
Answer: Both f calls compile. f(x) deduces T = int&, f(5) deduces T = int.
- Why should you not use
std::moveinreturnstatements of forwarding functions? Answer:std::moveprevents copy elision.std::forwardis also unnecessary since return statements already handle value categories correctly.
FAQ
Mini Project
Build a generic Delegate class that stores a callable and forwards arguments:
#include <iostream>
#include <string>
#include <functional>
// Your Delegate class with perfect forwarding
int main() {
Delegate<void(int, double)> d;
d.bind([](int a, double b) {
std::cout << "Sum: " << a + b << "\n";
});
int x = 5;
d.invoke(x, 3.14); // Forward x as lvalue
d.invoke(10, 2.71); // Forward as rvalues
// Also works with move-only types
Delegate<void(std::unique_ptr<int>)> moveOnly;
moveOnly.bind([](std::unique_ptr<int> p) {
std::cout << "Value: " << *p << "\n";
});
moveOnly.invoke(std::make_unique<int>(42));
}
This project mirrors how C++ standard library uses perfect forwarding in std::function, std::bind, and std::thread. Compare with Java which lacks value category distinction entirely.
What's Next
You now master perfect forwarding — the key to writing generic C++ wrappers. Next, you will learn structured bindings (C++17), which decompose tuples, pairs, arrays, and structs into named variables with clean syntax.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro