Recommended resources
The freely available resources this lesson will direct you to.
- C++ Perfect Forwardingwww.youtube.com
- Why use forwarding references and how they are different from rvalue references in C++www.youtube.com
- Rvalue reference declarator: && | Microsoft Learnlearn.microsoft.com
- Perfect forwarding and universal references in C++ - Eli Bendersky's websiteeli.thegreenplace.net
Hello! Last lesson established a subtle rule: even when a parameter has type T&&, its name is an lvalue expression inside the function. That matters when a function’s job is to pass an argument onward. Without care, a temporary that selected an rvalue overload at the wrapper’s boundary will select an lvalue overload at the next call.
In about 40 minutes, you’ll implement a wrapper that preserves the caller’s ability to pass mutable lvalues, const lvalues, and rvalues to another function. This is a common requirement in generic constructors and library adapters, where an unintended copy can be both a correctness issue and a performance cost.
The problem at the second call
Suppose deliver has different overloads for objects that can and cannot be moved from:
void deliver(std::string&);
void deliver(const std::string&);
void deliver(std::string&&);
This wrapper accepts only rvalues, but still passes its argument to deliver as an lvalue:
void relay(std::string&& message) {
deliver(message); // message is a named expression: an lvalue
}
Writing deliver(std::move(message)) would restore the rvalue call, but it would be wrong for a generic wrapper that must also accept lvalues without treating them as expendable. Taking the parameter by value might introduce a copy or move before deliver even runs. Taking it by const reference would prevent deliver from modifying a mutable argument or moving from a temporary.
Watch C++ Perfect Forwarding by BitsOfQ for the forwarding problem and its solution. The video’s explanation of std::forward uses conceptual pseudocode; the key point is that its result depends on the deduced template argument, not on a runtime test.
Watch C++ Perfect Forwarding by BitsOfQ to see why ordinary reference parameters cannot transparently relay every kind of argument.
In the problem, compare the failures of mutable and const lvalue references. In deduction, focus on how a deduced T&& accepts both lvalues and rvalues. In forwarding, watch how std::forward<T> restores the appropriate category when the named parameter is passed onward.
Why a deduced T&& can receive both kinds of argument
In this function template, T is deduced from each call:
template<class T>
void relay(T&& message);
Here T&& is a forwarding reference, not an rvalue reference restricted to rvalue arguments. Deduction and reference collapsing work together:
Argument passed to relay |
Deduced T |
Parameter type after collapsing |
|---|---|---|
Mutable std::string lvalue |
std::string& |
std::string& |
const std::string lvalue |
const std::string& |
const std::string& |
std::string rvalue |
std::string |
std::string&& |
const std::string rvalue |
const std::string |
const std::string&& |
For the first row, substituting T = std::string& gives a conceptual std::string& &&. Reference collapsing makes that std::string&. The compact rule is: if either reference is &, the result is &; only && combined with && remains &&. These combined forms arise during deduction—you do not write & && directly as an ordinary type declaration.
Read the Perfect forwarding section and the deduction discussion in Microsoft Learn’s Rvalue reference declarator: &&. Its factory example uses raw pointers; concentrate on how its arguments reach the constructor, not on adopting raw-pointer ownership in new code. The text briefly describes rvalue deduction in terms of an rvalue reference; for the T&& template above, keep the more precise distinction in the table: for an rvalue of type U, T is deduced as U, making the parameter U&&.
Rvalue reference declarator: && | Microsoft Learn
Read Microsoft Learn’s Rvalue reference declarator: && to connect the deduction rules to a two-argument constructor wrapper.
In “Perfect forwarding,” read the motivation. Continue from “Suppose you want to write a generic function that generates objects” through the factory example; compare its first A1&, A2& version with its A1&&, A2&& version. Then, under “Properties of rvalue references,” find “Function templates deduce their template argument types and then use reference collapsing rules.” Read the deduction explanation. Skip the long S specialization example and find the collapsing table. Check that its four rows agree with the compact rule above.
There is an important boundary on the terminology: T&& is a forwarding reference here because T is a deduced type parameter of this function template. A fixed std::string&& is an ordinary rvalue reference. So is const T&& for this purpose; adding const to the parameter pattern does not give it forwarding-reference deduction.
Implement the relay with std::forward
Deduction records how the caller supplied the argument, but it does not change the rule from the previous lesson: the expression message is an lvalue inside relay in every instantiation. The wrapper must use the deduced T when it makes its second call:
#include <iostream>
#include <string>
#include <utility>
void deliver(std::string&) {
std::cout << "mutable lvalue\n";
}
void deliver(const std::string&) {
std::cout << "const reference\n";
}
void deliver(std::string&&) {
std::cout << "mutable rvalue\n";
}
template<class T>
void relay(T&& message) {
deliver(std::forward<T>(message));
}
int main() {
std::string live{"bid"};
const std::string frozen{"ask"};
relay(live); // mutable lvalue
relay(frozen); // const reference
relay(std::string{"trade"}); // mutable rvalue
relay(std::move(live)); // mutable rvalue
relay(std::move(frozen)); // const reference
}
std::forward<T>(message) uses the T deduced at the call to relay. If T is std::string&, its result is an lvalue. If T is std::string, its result is an xvalue. If T is const std::string, its result is a const xvalue; that still cannot bind to the mutable std::string&& overload, so deliver(const std::string&) is selected.
A prvalue such as std::string{"trade"} is not literally passed onward as a prvalue: once the wrapper names its parameter, forwarding presents it as an xvalue. For these overloads, that preserves the relevant rvalue behavior. Neither std::forward nor entry into the std::string&& overload performs a move by itself; a move happens only if the receiving code constructs or assigns an object accordingly.
Why not replace std::forward<T>(message) with std::move(message)? For relay(live), std::move would send the caller’s still-live lvalue to the rvalue overload. The wrapper would silently grant the callee an opportunity to take its resources. std::forward<T> makes that choice conditional on what the caller passed.
Scale the same pattern without losing ownership boundaries
Each forwarded parameter needs its own deduced type. A two-argument wrapper has the shape:
template<class A, class B>
void relay_pair(A&& first, B&& second) {
receive(std::forward<A>(first),
std::forward<B>(second));
}
A mutable lvalue for first and a temporary for second can therefore reach different reference overloads independently. The same pattern extends to a parameter pack; std::make_unique is a standard-library example of forwarding constructor arguments:
#include <memory>
#include <utility>
template<class T, class... Args>
std::unique_ptr<T> create_owned(Args&&... args) {
return std::make_unique<T>(std::forward<Args>(args)...);
}
create_owned is illustrative—you would ordinarily call std::make_unique directly unless your wrapper has another responsibility. Notice the separate decisions: std::unique_ptr<T> owns the result, while forwarding controls how each argument is presented to T’s constructor. Forwarding does not itself copy, move, or extend the lifetime of an argument for later use. If a constructor stores a reference to a temporary passed through this wrapper, that reference can still dangle.
In code review, check both sides of the boundary: is the parameter really a forwarding reference with a deduced type, and is each named parameter passed exactly where intended using std::forward with its corresponding deduced type? Forwarding the same potentially movable argument to multiple consuming operations deserves particular scrutiny.
Conclusion
A forwarding wrapper needs two cooperating mechanisms. A deduced T&& accepts an lvalue or rvalue while retaining the distinction in T; std::forward<T>(parameter) uses that distinction when the named, otherwise-lvalue parameter is passed onward. Use std::move when you unconditionally intend to offer an object’s resources, and std::forward when a generic wrapper should preserve the caller’s choice.
The next lesson builds on this template pattern: C++20 concepts let you state which argument types a generic function actually accepts, rather than discovering an unsuitable type only through errors inside the wrapper.