AlgoMaster Logo

Perfect Forwarding (std::forward)

Medium Priority20 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

The previous lesson introduced rvalue references and showed how move semantics avoid unnecessary copies. This chapter is about a related problem that move semantics alone can't solve: writing a generic function that takes arguments and hands them off to another function while preserving exactly what the caller passed, including whether each argument was an lvalue or rvalue, and whether it was const. The tool C++ gives you for that is std::forward, and the language feature that makes it work is the forwarding reference, T&& in a deduced context.

The Forwarding Problem

Consider a factory function for orders in an e-commerce system. The Order class has several constructors: one that takes a customer name by value, one that moves a name in, one that takes a const reference, and maybe one that takes a temporary. A makeOrder helper constructs an Order and returns it, no matter how the caller wants to pass the arguments.

A first attempt that looks reasonable but isn't:

The first call is fine; the caller passed an lvalue, so we copy. The second call is wasteful. The caller passed a temporary std::string, which means Order(std::string&&) could have moved the contents in. But makeOrder took the argument as const std::string&, which is an lvalue inside the function, so the copying constructor was picked. The factory has erased the rvalue-ness of what the caller passed.

This specific case can be fixed by writing two overloads of makeOrder, one taking const T& and one taking T&&. For one parameter, two overloads. For two parameters, four overloads. For N parameters, 2^N overloads. A factory like std::make_shared that takes any number of constructor arguments couldn't possibly enumerate them all. A single template that adapts to whatever the caller did is needed.

Forwarding References (Universal References)

The construct that makes this work: if a function template has a parameter of the form T&& where T is a template parameter being deduced, that parameter is not an rvalue reference. It's a forwarding reference. Scott Meyers popularized the name "universal reference" in his books before the standard committee settled on "forwarding reference"; both names appear in older material, but they mean the same thing.

The same parameter arg accepted three different kinds of arguments. The third call passed a string literal, which is an array of char, and T was deduced as const char(&)[8], giving you a reference to the literal. The first two calls deduced T as std::string& and std::string respectively. That deduction asymmetry is the key to everything that follows.

The rules are precise, and they only apply when the compiler is deducing T for that parameter.

  • If you call inspect(name) with an lvalue, T is deduced as std::string&.
  • If you call inspect(std::string("Akira")) with an rvalue, T is deduced as std::string (no reference).

Plug those back into T&&. For the lvalue case, the parameter type becomes std::string& &&, which collapses to std::string&. For the rvalue case, it becomes std::string&&. The parameter changes its identity depending on what was passed. That's why the same syntax T&& works for both.

This deduction trick only happens in a deduced context. The form T&& in places where T is not being deduced is an rvalue reference. For example, void f(std::string&& s) is a plain rvalue reference parameter, and so is void g(std::vector<T>&& v) inside a class template where T was deduced earlier from the class. The forwarding reference rule fires only when this particular call site is what's deducing T.

The first T&& is an rvalue reference, because by the time push is called, T is already fixed by whoever instantiated Container<T>. The second is a forwarding reference. The distinction is subtle; the rule is "deduction happens at this call, or it doesn't".

Reference Collapsing Rules

The forwarding reference trick depends on a set of compiler rules called reference collapsing. The standard says that whenever you nominally end up with a "reference to a reference" inside a type, the compiler collapses it according to a fixed table. There are exactly four cases.

If the inner type isAnd the outer reference isThe result is
T&&T&
T&&&T&
T&&&T&
T&&&&T&&

The pattern: the only way the result is T&& is when both pieces are &&. Any time an lvalue reference shows up on either side, the result is an lvalue reference. C++ doesn't let you write T& & in source code; reference collapsing only happens through template type deduction, typedef, or using aliases.

The first call passes an lvalue, so T is deduced as int&. The parameter type is int& &&, which collapses to int&. The second call passes a literal 10, an rvalue, so T is deduced as plain int, and the parameter stays int&&. The third call uses std::move, which is a cast to rvalue, so T is int again. T itself can be a reference type; this is the whole point. The information about whether the original argument was an lvalue or rvalue is encoded in whether T is a reference type after deduction.

The same idea as a diagram:

The diagram traces both branches. When the caller passes an lvalue, T ends up being a reference type, and the parameter collapses back to a plain lvalue reference. When the caller passes an rvalue, T ends up being a non-reference type, and the parameter keeps its rvalue-reference shape. The function gets the same kind of reference the caller had.

There's still one problem to solve. Inside the function body, the parameter arg is a named variable. Named variables are lvalues, regardless of what type they have. So even if arg is typed as string&&, writing someFunction(arg) would pass it as an lvalue to someFunction. A way to recover the original value category when handing arg off is needed. std::forward provides that.

What std::forward Actually Does

std::forward<T>(arg) is a conditional cast. If T is an lvalue reference type, it returns an lvalue reference. Otherwise, it returns an rvalue reference. That's the entire job. The implementation in the standard library is essentially this:

The return type T&& carries the rule. By reference collapsing, when T is an lvalue reference like std::string&, the return type is std::string& && which collapses to std::string&, an lvalue reference. When T is a non-reference type like std::string, the return type stays std::string&&, an rvalue reference. So std::forward<T> casts back to whatever the original argument was.

One critical detail: T must be passed explicitly to std::forward. The template argument is what carries the original value category information; it can't be deduced from arg alone, because inside the function arg is always an lvalue. The conventional call is std::forward<T>(arg), mirroring how T&& arg was declared.

That's perfect forwarding in one example. relay is a generic wrapper that takes anything, and calls handle with the same value category the caller provided. The lvalue case picks the const-reference overload; the rvalue cases both pick the rvalue-reference overload, even though by the time we're inside relay, arg is a named variable.

Compare what happens without the cast:

What's wrong with this code?

arg is a named parameter inside relay, so the expression arg is an lvalue regardless of how arg was declared or what was passed in. Without std::forward, the call to handle(arg) always goes to the const std::string& overload. The rvalue-ness is lost the moment the argument gets a name.

std::forward vs std::move

Both std::move and std::forward are casts. They look similar, and both live in <utility>, but they do different jobs.

std::move(x) is an unconditional cast to T&&. It always produces an rvalue reference. Use it when you want to give up ownership of x to the next function. After std::move(x), the value of x is unspecified but valid; the moved-from object can still be destroyed and assigned to, but you shouldn't read its old value.

std::forward<T>(x) is a conditional cast that depends on what T is. If T is an lvalue reference type, it produces an lvalue reference. If T is a non-reference type, it produces an rvalue reference. Use it inside a function template that took a forwarding reference, to pass the argument along without changing its category.

Propertystd::movestd::forward<T>
What it casts toAlways rvalue referenceLvalue or rvalue ref depending on T
When to useYou want to move from xYou want to pass x along unchanged
Template argumentNone neededRequired (the original T)
Typical contextMove constructors, move assignment, sinking argumentsForwarding functions, factories, wrappers
Effect on the sourceSource becomes "moved-from" if the target steals from itNo effect on the source unless T was deduced as a non-reference and the callee moves

Using the wrong one is a real mistake. std::move inside a forwarding wrapper turns lvalues into rvalues without permission. The caller passed name expecting it to stay valid, and the wrapper told the next function "you can steal from this". That's a bug, not just a stylistic issue.

wrapBad stole the contents of name because of the unconditional std::move. wrapGood left name2 alone, because the caller passed an lvalue and std::forward correctly cast it to an lvalue. The user of wrapGood would have to write wrapGood(std::move(name2)) themselves if they wanted to move; the wrapper doesn't override that decision.

Getting this wrong doesn't make code slower, it makes code incorrect. Using std::move instead of std::forward in a wrapper means objects get moved-from when the caller didn't ask for that. The next read of the supposedly-untouched original is a bug in the library, not in the caller's code.

Building a Factory Function

makeOrder can now be written properly. The factory takes any number of constructor arguments, in any combination of value categories, and constructs an Order from them with no unnecessary copies or moves.

The first call passes an lvalue and gets the copying constructor. The second call passes a temporary and gets the moving constructor. The third call passes a string literal, which is a const lvalue array, and falls through to the const-reference constructor after the implicit string construction. The factory does no extra work in any case.

The function takes its parameter as Name&& (forwarding reference) and passes it through with std::forward<Name>. The shape of the call site at line Order(std::forward<Name>(customer), orderId) is the canonical perfect-forwarding pattern, appearing in nearly every standard library factory.

This factory only takes one templated argument. Real factories like std::make_unique, std::make_shared, and vector::emplace_back need to forward any number of arguments. Variadic templates handle that.

Variadic Perfect Forwarding

A variadic template parameter typename... Args lets a function accept any number of arguments of any types. Combined with forwarding references and std::forward, an arbitrary argument list can be forwarded without losing any value category.

This is, in essence, the implementation of std::make_unique from C++14. The parameter pack Args&&... args is a list of forwarding references, one per argument. The call expression std::forward<Args>(args)... is a pack expansion: the compiler generates one std::forward<Argi>(argi) for each argument in the pack. Each one independently preserves the value category of its corresponding caller argument.

The mechanics of pack expansion: the expression std::forward<Args>(args)... doesn't mean "apply forward once and then expand"; it means "for each i, generate std::forward<Args_i>(args_i), and separate them by commas". So if the caller passed three arguments, the expansion produces three forward calls inside the constructor call. Each one carries its own deduced T and its own original value category. This is why you write std::forward<Args>(args)... and not std::forward<Args...>(args...); the latter would treat the whole pack as one template argument, which doesn't type-check.

Perfect forwarding has zero runtime overhead in optimized builds. std::forward is a static_cast, which is erased by the compiler. Every example in this chapter compiles to the same assembly the hand-rolled overloads would produce, without the combinatorial explosion of writing them.

The Order constructor takes std::string customer by value and moves it into the member. That's a common pattern called "pass by value and move" or the "sink" idiom. The constructor accepts a string however the caller wants to provide it: if the caller had an lvalue, the value parameter is copy-constructed; if the caller had an rvalue, it's move-constructed. Either way, the move into customer_ is cheap. Combined with makeUnique's perfect forwarding, you get one copy for lvalue inputs and zero copies for rvalue inputs, which is the optimum.

A Generic Logging Wrapper

A larger example shows why perfect forwarding is useful beyond factories. A function processOrder wrapped with a logger. The logger accepts any callable and any argument list, logs the call, invokes the callable, and returns the result.

withLogging takes a callable Fn&& and a parameter pack Args&&... by forwarding reference, logs a tag, then forwards everything to the callable. The return type is computed by decltype on the forwarded call. In C++14 and later, writing auto and letting the return type deduction work it out is shorter, but the explicit trailing return type makes the mechanism visible.

The second call: the caller wrote std::move(token) to pass the token as an rvalue, and chargeCard takes its first parameter as std::string&&. The forwarding wrapper preserved that rvalue category, so the function received the same kind of reference it would have if the caller had called it directly. If withLogging had taken Args... by value (or by const reference), the rvalue-ness would have been lost and chargeCard would have failed to compile (because you can't bind an rvalue reference to an lvalue).

The same wrapper works for any callable, including lambdas, functors, and member functions invoked through std::invoke (since C++17). It's roughly the architecture of std::bind, std::async, and the executor adapters in the standard library.

Common Pitfalls

Perfect forwarding has a small number of well-known traps. Each of them looks reasonable and breaks in a way that's hard to debug if you don't know about it.

Using std::move instead of std::forward

This was covered above, but it's worth repeating because it's the most common mistake. Inside a forwarding wrapper, never write std::move(arg). It turns lvalues into rvalues and moves out of objects the caller still owns.

The fix is always std::forward<T>(arg). If the wrapper wants to always move, take the parameter as T (by value) or T&& (rvalue reference, not forwarding reference) and document the intent.

Calling std::forward twice on the same argument

std::forward<T>(arg) doesn't move anything; it's a cast. The move happens when whoever receives the rvalue reference decides to construct or assign from it. But if you forward the same argument to two different functions, the second one might find an already-moved-from object.

What's wrong with this code?

If T is deduced as a non-reference type (rvalue case) and handle moves its argument, the second call to handle receives a moved-from object. The cast itself doesn't move, but the receiving function does. The fix is to forward once and copy or otherwise reuse the result for subsequent calls.

Applying std::forward in the wrong place

std::forward belongs at the call site where the argument is being passed to the next function, not anywhere else. Writing auto x = std::forward<T>(arg); and then using x elsewhere doesn't help, because x is a named variable from there on; the rvalue-ness it carried has already been used (or wasted).

Greedy forwarding constructors that hijack copy and move

This is the subtle one. A class with a constructor that takes a forwarding reference will sometimes get picked over the implicitly generated copy constructor.

Order b = a; looks like a plain copy, and a class with only the lvalue constructor would copy fine. But here, overload resolution prefers the forwarding constructor because deducing T = Order& gives an exact match, whereas the copy constructor would have to bind Order& to a const Order& parameter (a worse match). The forwarding constructor is greedier. The fix is to constrain it.

Fix (since C++17 using SFINAE):

The enable_if_t disables the template when T would be Order (or a reference to one). With C++20 concepts, you'd write requires (!std::is_same_v<std::decay_t<T>, Order>), which reads more clearly. The point is the same: greedy forwarding constructors must explicitly excuse themselves from cases the class's own copy and move constructors should handle.

const T&& and why it's strange

const T&& is almost never useful as a parameter type. An rvalue reference to a const object is technically valid but rarely useful, because the whole point of taking an rvalue reference was to move from the source. You can't move from a const source. The standard library uses const T&& in a few places (such as std::optional::value()) to deliberately disable certain overloads, but in user code it's almost always a sign that something else is wrong.

Inside a forwarding template, don't write const T&& explicitly. The deduction machinery handles const correctly: if the caller passes a const std::string&, T is deduced as const std::string&, and T&& collapses to const std::string&. Forwarding preserves the const-ness automatically. Second-guessing the deduction by writing const T&& somewhere usually breaks things.

A greedy forwarding constructor that hijacks copies can cause real performance regressions, not just compilation errors. It might compile fine and pick the forwarding template every time, doing slightly more work than the trivial copy would. Always constrain forwarding constructors when the class is meant to be copyable.

Quiz

Perfect Forwarding Quiz

10 quizzes