Perfect Forwarding and std::forward
An introduction to problems that can arise when our functions forward their parameters to other functions, and how we can solve those problems with std::forward
In this lesson, we'll introduce how std::forward
optimizes argument passing between functions. We'll see how forwarding arguments from one function to another can cause our program to run slower than we might have intended.
We'll then see how we can solve this problem using std::forward
, with practical examples including template functions and variadic functions
This lesson builds upon our earlier sections that covered copy and move semantics. Familiarity with these concepts will be helpful, but we'll quickly review the key points.
A Review of Move Semantics
In our previous lesson on move semantics, we discussed the cost associated with copying objects, particularly those with extensive nested resources. Move semantics provide a solution by allowing the direct transfer of resources to a new object, bypassing the need for complete duplication.
Because this process involves transferring resources away from our original object, this process leaves the object in a degraded, "moved-from" state. But, in scenarios where the original object is no longer required, this is perfectly acceptable.
The decision on whether and how to implement move semantics is based on the specific requirements of our class and the nature of its resources. A minimalist example is shown below:
1#include <iostream>
2
3class Character {
4public:
5 Character(){}
6
7 // Copy Constructor
8 Character(const Character& Original){
9 std::cout << "Copying\n";
10 }
11
12 // Move Constructor
13 Character(Character&& Original){
14 std::cout << "Moving\n";
15 }
16};
17
18int main(){
19 Character A;
20 Character B{A};
21 Character C{std::move(A)};
22}
In this example, the Character
we pass to the constructor on line 20 is an lvalue.
On line 21, by wrapping the argument in std::move()
, we cast it to an rvalue. This tells the compiler it's safe to "move from" the character A
, and therefore the move constructor is invoked.
The move constructor is the constructor that accepts an rvalue reference, denoted by the &&
on line 13. Our previous program had the following output:
Copying
Moving
Objects can also be copied or moved using the assignment operator, eg B = A
. To fully implement copy and move semantics, we'd want to implement those operators too.
For this lesson, we'll just implement simple constructors, as our focus is on forwarding rather than semantics. We have dedicated lessons on and earlier in the course:
Forwarding References
Correctly implementing move semantics has some challenges when there is an intermediate function that exists between our calling code and our constructor. We've seen examples of this in the standard library.
For example, functions like std::make_unique()
and std::make_shared()
receive a list of arguments that are eventually going to be forwarded to a constructor.
The emplace()
method that exists on various containers like arrays and linked lists has similar requirements.
Let's imagine we need to create such an intermediate template function. This will let us see some of the challenges, and how we can solve them. We'll call ours Build()
:
#include <iostream>
class Character {/*...*/};
template <typename T>
T Build(T& Original){
T NewObject{Original};
return NewObject;
}
int main(){
Character A;
Character B{Build(A)};
Character C{Build(std::move(A))};
}
Our first challenge is that our reference parameter cannot accept an rvalue reference at all. We get the compilation error:
A non-const reference may only be bound to an lvalue
To fix this, we can update our parameter list to accept a new type of reference - a forwarding reference using &&
:
template <typename T>
T Build(T&& Original){
T NewObject{Original};
return NewObject;
}
This may be confusing, as we previously showed that &&
denotes an rvalue reference. However, when used with a template type (including an auto
function parameter), &&
denotes the type is forwarding reference.
A forwarding reference can bind to both lvalues and rvalues, preserving their value category:
#include <iostream>
class Character {/*...*/};
template <typename T>
T Build(T&& Original){
T NewObject{Original};
return NewObject;
}
int main(){
Character A;
Character B{Build(A)};
Character C{Build(std::move(A))};
}
Our code now compiles, but we've lost the benefits of move semantics. Our object is being copied, regardless of whether we provide an lvalue or rvalue. The output of the previous code example is:
Copying
Copying
We'll fix this soon, but lets first introduce some helpful type traits.
Using std::remove_reference
and std::decay
Our Build()
function now compiles successfully, but its behavior will be a bit confusing. This is because the template type T
will sometimes be a value type (as in Character
) and sometimes a reference type (as in Character&
or Character&&
). The possibility of each of these types also being const
adds further complexity.
What is our Build()
function actually building and what is it returning in each of these scenarios? The answer to that question is not exactly clear, as it depends on intricate details on how references get constructed from other references.
This is called reference collapsing, for those who want to investigate the underlying rules governing situations such as this. However, we don't want the behavior of our functions to be defined by esoteric rules if we can instead be clear and explicit about how our code is intended to work.
For example, let's imagine we want our Build()
function to build a new object and return it by value. Ideas we introduced in our lessons can help us implement this behavior unambiguously.
Specifically, if T
is a reference type, we want to construct and return the equivalent value type. The standard library already has the std::remove_reference
utility for exactly this scenario.
As usual, we access it through the <type_traits>
header, and we retrieve the resulting type from its type
static member, or by using the _t
variation:
#include <type_traits>
class Character{};
// It echoes non-reference types back to us
// Therefore, T1 will be Character
using T1 = std::remove_reference<Character>::type;
// If we provide it a reference type, it returns
// the corresponding value type
// Therefore, T2 will also be Character
using T2 = std::remove_reference<Character&>::type;
// We can use the _t syntax instead of ::type
using T3 = std::remove_reference_t<Character&>;
We also have the option of using its stronger std::decay
friend. In addition to removing any reference component on the type, std::decay
will also remove any const
or volatile
qualifiers:
#include <type_traits>
class Character{};
// T4 will be Character
using T4 = std::decay_t<const Character&>;
Let's apply this to our Build()
template. Instead of constructing and returning a T
, we'll construct and return a decayed T
using std::decay_t<T>
:
#include <iostream>
#include <type_traits>
class Character {/*...*/};
template <typename T>
std::decay_t<T> Build(T&& Original) {
std::decay_t<T> NewObject{Original};
return NewObject;
}
int main() {/*...*/};
We can now also remove the intermediate NewObject
local variable:
#include <iostream>
#include <type_traits>
class Character {/*...*/};
template <typename T>
std::decay_t<T> Build(T&& Original) {
return std::decay_t<T>(Original);
}
int main() {/*...*/};
Our Build()
function is now explicitly constructing and returning an object by value, but it's still ignoring the type's move semantics:
Copying
Copying
Let's fix that next.
Perfect Forwarding using std::forward()
The fact that our Build()
function is always calling the lvalue version of our Character
constructor is perhaps not surprising if we recall that a function argument and the associated parameter are not the same variables.
Even though the argument provided to Build()
may be an rvalue returned from std::move()
, within the context of the Build()
function, the parameter Original
is an lvalue. It has a name (Original
) and a memory address (&Original
).
We could of course cast it back to an rvalue using std::move()
before forwarding it:
template <typename T>
std::decay_t<T> Build(T&& Original) {
return std::decay_t<T>(
std::move(Original)
);
}
But we want our function to respect the original type that was provided as an argument. We don't want to assume it's always going to be a rvalue
Perfect forwarding is the process of forwarding arguments to other functions in a way that respects their original value category - whether they are lvalues or rvalues.
Wrapping an argument with the std::forward()
function is the easiest way to implement this. std::forward()
receives the type of the object as a template parameter, and the object itself as a function parameter:
#include <iostream>
#include <type_traits>
class Character {/*...*/};
template <typename T>
std::decay_t<T> Build(T&& Original) {
return std::decay_t<T>(
std::forward<T>(Original)
);
}
int main() {/*...*/};
Our basic Build()
function is now working correctly, complete with perfect forwarding:
Copying
Moving
Using std::forward()
with Variadic Functions
One of the most common use cases for variadic functions is to implement the forwarding behavior seen in functions like std::make_unique()
and the emplace()
method on types like std::vector
.
These functions cannot know in advance how many constructor arguments the underlying type will support. Therefore, they need to be implemented as , accepting any number of arguments.
Let's do the same for our Build()
function:
#include <iostream>
#include <type_traits>
class Character {/*...*/};
template <typename T, typename... Args>
std::decay_t<T> Build(Args&&... Arguments){
return std::decay_t<T>{
Arguments...
};
}
int main(){
using namespace std::string_literals;
auto A{Build<Character>("Legolas"s, 100)};
auto B{Build<Character>(A)};
auto C{Build<Character>(std::move(A))};
}
These arguments are being forwarded to a Character
constructor but, without perfect forwarding, we've lost the benefits of move semantics:
Constructing Legolas
Copying Legolas
Copying Legolas
To fix this, we can update our function to use std::forward()
at the location where we're expanding our parameter pack:
#include <iostream>
#include <type_traits>
class Character {/*...*/};
template <typename T, typename... Args>
std::decay_t<T> Build(Args&&... Arguments){
return std::decay_t<T>{
std::forward<Args>(Arguments)...
};
}
int main() {/*...*/};
Constructing Legolas
Copying Legolas
Moving Legolas
Summary
In this lesson, we've introduced perfect forwarding, and how std::forward()
can help us implement it. The key takeaways include:
- The basics of move semantics and how they allow for resource-efficient object management by transferring rather than copying data.
- The introduction and application of forwarding references, which allow templates to handle both lvalue and rvalue references.
- Using
std::forward()
to maintain the original value category of arguments, ensuring that our functions can perfectly forward arguments to other functions, preserving efficiency. - A common use case for variadic functions is to collect arguments to be forwarded to some other function, and how to use
std::forward()
in that context.
Iterators and Ranges
This lesson offers an in-depth look at iterators and ranges, emphasizing their roles in container traversal