Question
std::forward in C++: Perfect Forwarding, Purpose, and Problems Solved
Question
In a perfect-forwarding function, std::forward is used to convert the named rvalue-reference parameters t1 and t2 back to their original value categories before passing them to inner.
What is the purpose of doing this, and which problems does it solve? What effect would it have on overload resolution and the behavior of inner if t1 and t2 were passed directly as lvalues?
template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2)
{
inner(std::forward<T1>(t1), std::forward<T2>(t2));
}
Short Answer
std::forward preserves whether an argument was originally an lvalue or an rvalue. It is the key operation in perfect forwarding: a wrapper function can pass arguments to another function without accidentally turning every argument into an lvalue.
By the end, you will understand why named rvalue-reference variables are lvalues, how std::forward<T>(value) works, and when forwarding is appropriate.
Concept
A value's value category affects what a function can do with it:
- An lvalue has an identity and can usually be used again, such as a named variable.
- An rvalue is commonly a temporary value that may safely have resources moved from it.
C++ can overload functions based on these categories:
void inner(const std::string& text); // reads or copies
void inner(std::string&& text); // may move from text
A parameter declared as T&& in a deduced template context is a forwarding reference (previously called a universal reference). It can bind to either category:
std::string name = "Ada";
outer(name); // T is std::string&, parameter is std::string&
outer(std::string("Ada")); // T is std::string, parameter is std::string&&
However, there is an important rule: every named variable expression is an lvalue, even when its declared type is an rvalue reference.
void example(std::string&& value)
{
// `value` has type std::string&&,
// but the expression `value` is an lvalue.
}
Therefore, calling inner(t1) directly always passes an lvalue expression. std::forward<T1>(t1) conditionally casts t1:
- If the original caller supplied an lvalue, it remains an lvalue.
- If the original caller supplied an rvalue, it becomes an rvalue again.
This lets wrapper functions, factory functions, containers, and adapters preserve the caller's intent. In particular, it allows an eventual target function to choose move-aware overloads for temporary objects while continuing to treat reusable named objects as lvalues.
Mental Model
Think of outer as a delivery service receiving packages.
- An lvalue is a package the sender still owns and may want back. The delivery service should handle it carefully and not treat it as disposable.
- An rvalue is a temporary package the sender has finished with. The receiver may unpack it and reuse its contents.
When the package arrives at outer, it gets a label: t1. Because it now has a name, C++ treats it as a reusable package—an lvalue—even if it was originally temporary.
std::forward<T1>(t1) checks the original delivery instructions stored in T1 and restores them for inner: “this was borrowed” or “this may be consumed.”
Syntax and Examples
The normal perfect-forwarding pattern is:
template <typename T>
void outer(T&& value)
{
inner(std::forward<T>(value));
}
Use the same template type parameter in both places:
std::forward<T>(value)
Here is a complete example with overloads:
#include <iostream>
#include <string>
#include <utility>
void inner(const std::string& text)
{
std::cout << "inner received an lvalue: " << text << '\n';
}
void inner(std::string&& text)
{
std::cout << "inner received an rvalue: " << text << '\n';
}
template <typename T>
void outer(T&& text)
{
inner(std::forward<T>(text));
}
int main()
{
std::string name = "Ada";
outer(name); // calls inner(const std::string&)
outer(std::string("Grace")); // calls inner(std::string&&)
}
std::forward does not inherently move an object or destroy it. It preserves the category encoded in T, allowing the receiving function to decide whether it should copy, read, or move from the argument.
Include <utility> to use std::forward.
Step by Step Execution
Consider this program:
#include <string>
#include <utility>
void inner(const std::string& value);
void inner(std::string&& value);
template <typename T>
void outer(T&& value)
{
inner(std::forward<T>(value));
}
int main()
{
std::string title = "Report";
outer(title);
outer(std::string("Draft"));
}
First call: outer(title)
titleis a named variable, so it is an lvalue.- Template deduction makes
Tequal tostd::string&. - Reference collapsing makes
T&&becomestd::string&;valuerefers totitle. std::forward<std::string&>(value)produces an lvalue.- The lvalue overload,
inner(const std::string&), is selected.
Second call: outer(std::string("Draft"))
std::string("Draft")is a temporary, so it is an rvalue.- Template deduction makes
Tequal tostd::string. - The parameter type is
std::string&&. - Although
valueis a named lvalue expression insideouter,std::forward<std::string>(value)produces an rvalue. - The rvalue overload,
inner(std::string&&), is selected.
Without std::forward, both calls use inner(value). Since value is named, both are treated as lvalues and the rvalue overload is not selected.
Real World Use Cases
std::forward is useful when a function receives arguments only to pass them onward.
-
Factory functions: Construct an object directly from arbitrary constructor arguments.
template <typename T, typename... Args> T make_object(Args&&... args) { return T(std::forward<Args>(args)...); } -
Container insertion: Forward a value to an element constructor so temporaries can be moved instead of copied.
template <typename T> void add_item(T&& item) { items.emplace_back(std::forward<T>(item)); } -
Callback wrappers: Pass an event or request to a callback while preserving whether it is temporary or reusable.
-
Logging and formatting wrappers: Relay formatting arguments to another API without creating unnecessary copies.
-
Dependency injection and builders: Forward constructor parameters through one or more wrapper layers to the final object.
Real Codebase Usage
In production C++ code, perfect forwarding is most common in generic utilities and constructors.
Forwarding a parameter pack
Each argument needs its corresponding template type:
template <typename Function, typename... Args>
decltype(auto) invoke_wrapper(Function&& function, Args&&... args)
{
return std::forward<Function>(function)(std::forward<Args>(args)...);
}
This allows the wrapper to preserve both the callable's category and every argument's category.
Forwarding constructors
A class may forward arguments to a member's constructor:
#include <string>
#include <utility>
class Message
{
public:
template <typename T>
explicit Message(T&& text)
: text_(std::forward<T>(text))
{
}
private:
std::string text_;
};
Passing a std::string lvalue copies its content into text_; passing a temporary std::string lets text_ use its move constructor.
Important design guideline
Use forwarding references when your function is a generic relay. If a function needs to store, inspect, or use a value more than once, taking a value by value or by const& is often clearer and safer. Perfect forwarding can make overload resolution and error messages more complex, so it should solve a real forwarding need.
Common Mistakes
Forgetting std::forward
This compiles, but it loses rvalue-ness:
template <typename T>
void outer(T&& value)
{
inner(value); // Always passes an lvalue expression.
}
Use std::forward<T>(value) when relaying value to another function.
Using std::move instead of std::forward
template <typename T>
void outer(T&& value)
{
inner(std::move(value)); // Wrong for a general forwarding wrapper.
}
std::move unconditionally produces an rvalue. If the caller passed an lvalue, this may allow inner to move from the caller's object unexpectedly. std::forward<T> is conditional and preserves the original category.
Forwarding with the wrong type
template <typename T>
void outer(T&& value)
{
inner(std::forward<std::string>(value)); // Usually wrong.
}
Use the deduced type parameter associated with that parameter:
inner(std::forward<T>(value));
Forwarding the same rvalue more than once
template <typename T>
void duplicate(T&& value)
{
first(std::forward<T>(value));
second(std::forward<T>(value)); // May observe a moved-from object.
}
If the original argument was an rvalue, first may move from it. Do not assume it still contains its original value afterward.
Treating T&& as always an rvalue reference
T&& is a forwarding reference only when T is deduced in the relevant context. For example, std::vector<T>&& is simply an rvalue reference; it does not accept lvalue vectors through the same deduction behavior.
Forwarding after using an object as if it were unchanged
A function can inspect an argument before forwarding it, but if it modifies it or relies on a stable value afterward, forwarding an rvalue may be inappropriate. Clearly define whether the function may consume its argument.
Comparisons
| Expression or technique | Result | Best use |
|---|---|---|
inner(value) | value is passed as an lvalue because it is named | The target should always borrow/read the object |
inner(std::move(value)) | Forces value to be treated as an rvalue | You deliberately want to allow moving, regardless of how it was received |
inner(std::forward<T>(value)) | Preserves the original lvalue/rvalue category | Generic forwarding wrappers |
const T& parameter | Binds to lvalues and rvalues but does not permit moving from the source | Read-only input |
T parameter | Makes a local copy or move of the argument | The function needs an owned local value |
std::move versus std::forward
Both are casts; neither moves data by itself. A move happens only if a selected operation, such as a move constructor, uses the resulting rvalue.
std::move(x)means: “Treatxas an rvalue now.”std::forward<T>(x)means: “Treatxas the category originally represented byT.”
For a forwarding-reference parameter, use std::forward<T>(x). For an object your function owns and no longer needs, use std::move(x).
Cheat Sheet
#include <utility>
template <typename T>
void wrapper(T&& value)
{
target(std::forward<T>(value));
}
T&&is a forwarding reference whenTis deduced.- A named parameter is always an lvalue expression, including a parameter declared as
Type&&. std::forward<T>(value)preserves the caller's original category.- Use
std::forwardwhen passing a forwarding-reference parameter onward. - Use the matching type:
std::forward<T>(value). - Use
std::movewhen you intentionally want to treat an object as expendable. - Forwarding an rvalue may let another function move from it.
- Do not forward the same potentially consumed rvalue repeatedly unless moved-from behavior is acceptable.
Reference collapsing rules:
| Written combination | Final type |
|---|---|
T& & | T& |
T& && | T& |
T&& & | T& |
T&& && | T&& |
FAQ
Why is a named rvalue reference an lvalue in C++?
A name identifies an object that can be used again within its scope. Therefore, an expression such as value is an lvalue even if value was declared as Type&&.
Does std::forward move an object?
No. It casts based on T. A later constructor, assignment operator, or function overload may move from the object if it receives an rvalue.
What happens if I omit std::forward?
The target function sees named parameters as lvalues. Rvalue overloads may not be selected, and temporary objects may be copied instead of moved.
When should I use std::move instead of std::forward?
Use std::move when your function owns an object and intentionally gives up its value. Use std::forward<T> when relaying a deduced T&& parameter while preserving the caller's category.
Can I use std::forward with an ordinary parameter?
You can, but it is mainly intended for forwarding-reference patterns. With an ordinary parameter, there is often no original category to preserve, and std::move or no cast may better communicate your intent.
Is const T&& a forwarding reference?
No. A forwarding reference has the form T&& where T is deduced. Adding const prevents the special lvalue-binding behavior.
Why is std::forward<T>(value) written with T explicitly?
The template argument tells std::forward whether value should become an lvalue or rvalue. It uses the type deduced when outer was called.
Mini Project
Description
Build a small forwarding wrapper that sends text to overloaded handlers. The project demonstrates how omitting std::forward causes a temporary string to be treated as an lvalue, while correct forwarding preserves move-aware behavior.
Goal
Create a relay function that calls the correct handle overload for both named strings and temporary strings.
Requirements
- Create one
handleoverload forconst std::string&and one forstd::string&&. - Implement a templated
relayfunction that accepts a forwarding reference. - Forward the argument from
relaytohandlewhile preserving its value category. - Call
relayonce with a namedstd::stringand once with a temporary string. - Print which overload handled each argument.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.