An argument passing function looks superficial. But the method of passing an argument silently makes the argument change its value category. The potential move constructor of an rvalue is turned into a copy constructor of an lvalue. Perfect forwarding does this.
Think about a tiny wrapper, which takes a value and outputs it unaltered.
#include <iostream>
#include <string>
// Make the following definition depend on template arguments
template <typename T>
T wrap(T value)
{
std::cout << value << '\n';
return value;
}
int main()
{
std::string s{"hello"};
wrap(s);
wrap(std::string{"world"});
return 0;
}
The argument value is passed by value, so both calls make a copy of the argument. The first makes a copy of the named string s, and the second makes a copy of the temporary. These extra copies can be seen in the output code even though the function just prints and returns.
If you were to pass the parameter as a normal reference, then this would prevent the copy of the named object but would bar the binding to the temporary.
#include <iostream>
#include <string>
template <typename T>
T wrap(T& value)
{
std::cout << value << '\n';
return value;
}
int main()
{
std::string s{"hello"};
wrap(s);
// wrap(std::string{"world"}); // error: cannot bind temporary to non-const reference
return 0;
}
The call with the named string goes, but the call with the temporary is malformed. A const reference would take both, but it would also avoid modification, and would still turn an rvalue into an lvalue when used as the argument for the next call.
A forwarding reference handles both issues simultaneously. It is written as T&&, where T is a template parameter to be deduced.
#include <iostream>
#include <string>
template <typename T>
// Accept a forwarding reference that can bind to lvalues or rvalues
T wrap(T&& value)
{
std::cout << value << '\n';
return value;
}
int main()
{
std::string s{"hello"};
wrap(s);
wrap(std::string{"world"});
return 0;
}
If the argument is the named lvalue s, then the compiler deduces T as std::string&. The parameter type becomes std::string& &&, collapsing to std::string&. This binds the named object directly, without copying.
If the argument is the temporary, then the compiler deduces T as std::string.
The parameter becomes std::string&&. The temporary binds to an rvalue reference and can be moved from later.
The return still turns the parameter back into an lvalue, so the temporary is copied rather than being moved out of the function. The last part of the technique is:
#include <iostream>
#include <string>
#include <utility>
template <typename T>
T wrap(T&& value)
{
std::cout << value << '\n';
// Preserve the original value category when forwarding
return std::forward<T>(value);
}
int main()
{
std::string s{"hello"};
wrap(s);
wrap(std::string{"world"});
return 0;
}
std::forward<T>(value) performs a conditional cast. If T was deduced to be an lvalue reference type, then std::forward will cast to produce an lvalue, whereas if it was deduced to be a non reference, then it will cast to produce an rvalue. In the second call, the temporary will thus be moved into the returned object instead of being copied into it. The first call still treats the named string as an lvalue, so it will only be copied if the caller requires a new object.
The identical pattern is applied to a function that takes the argument and sends it on to a different function instead of returning it.
#include <iostream>
#include <string>
#include <utility>
void process(std::string v)
{
std::cout << v << '\n';
}
template <typename T>
// Require both conditions joined by logical AND to be true
void forwardToProcess(T&& value)
{
process(std::forward<T>(value));
}
int main()
{
std::string s{"hello"};
forwardToProcess(s);
forwardToProcess(std::string{"world"});
return 0;
}
Inside the forwarding function forwardToProcess, the parameter is again a forwarding reference. The call to process passes the argument through std::forward<T>(value). If the original argument was an lvalue, the forwarded expression is an lvalue and process makes a copy.
If the original argument was an rvalue, the forwarded expression is an rvalue and process moves from it.
The wrapper itself never causes a copy or a move; it just recaptures what the caller has already supplied.
Forwarding reference is not an rvalue reference; it's a special kind of reference that shows up only when a template parameter is being deduced, and is written with the && token. The deduction rules, and the reference collapsing rules, allow a single function template to bind to both lvalues and rvalues, and then std::forward keeps the original category visible to the receiving function. It's this combination that is called perfect forwarding!
Exercise: Perfect Forwarding with Forwarding References and std::forwardStarter code, hints and solutionOpenClose
Practice: Perfect Forwarding with Forwarding References and std::forward
Work through the mission, reveal hints only when needed, and compare your result with the model solution.
Your challenge
Report the deduced lvalue/rvalue input category in forwardToProcess. Append ! inside process, then print the original s to show the effect of its by-value parameter.
Mission checklist
- Run the starter as a baseline, then predict the change requested in Your challenge.
- Make that change in your editor; use the lesson-specific hints if you need help.
- Compile and run your answer, then compare with the worked solution and explain the result.
Starter program
This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.
#include <iostream>
#include <string>
#include <utility>
void process(std::string v)
{
std::cout << v << '\n';
}
template <typename T>
// Require both conditions joined by logical AND to be true
void forwardToProcess(T&& value)
{
process(std::forward<T>(value));
}
int main()
{
std::string s{"hello"};
forwardToProcess(s);
forwardToProcess(std::string{"world"});
return 0;
}Hints
T&& is a forwarding reference in this deduced template. Passing the named string s and passing a temporary string lead to different deduced reference behavior.
std::is_lvalue_reference_v<T> distinguishes the two deduced forms. process still receives by value even when its argument was forwarded from a reference.
Worked solution
The forwarding function now reports whether the deduced T is an lvalue reference. s takes that path, while the temporary takes the rvalue path. process receives its own string by value and appends ! to that local copy or moved value; the original s remains hello.
#include <type_traits>
#include <iostream>
#include <string>
#include <utility>
void process(std::string v)
{
v += "!";
std::cout << v << '\n';
}
template <typename T>
// Require both conditions joined by logical AND to be true
void forwardToProcess(T&& value)
{
std::cout << (std::is_lvalue_reference_v<T> ? "lvalue input\n" : "rvalue input\n");
process(std::forward<T>(value));
}
int main()
{
std::string s{"hello"};
forwardToProcess(s);
forwardToProcess(std::string{"world"});
std::cout << "Original s: " << s << '\n';
return 0;
}
Example output
lvalue input
hello!
rvalue input
world!
Original s: hello
0 comments
No comments yet. Be the first.