Perfect Forwarding
Explore how perfect forwarding enables functions to forward arguments while preserving their lvalue or rvalue properties. Understand universal references and std::forward to implement efficient, generic factory methods that handle arbitrary arguments with optimized resource management in Modern C++.
If a function template forwards its arguments without changing their lvalue or rvalue characteristics, we call it perfect forwarding.
A Perfect Factory Method
Firstly, a short disclaimer: the expression a perfect factory method is not a formal term.
A perfect factory method is actually a generic factory method, meaning that the function should have the following characteristics:
- It can take an arbitrary number of arguments
- It can accept lvalues and rvalues as an argument
- It forwards it arguments identical to the underlying constructor
In other words, a perfect factory method should be able to create each arbitrary object.
Perfect forwarding enables it to write a function that can identically forward its arguments. In doing so, the lvalue and rvalue properties are respected.
Let’s start with the first iteration and move towards implementing the perfect factory method.
First Iteration
For efficiency reasons, the function template should take its arguments by reference. To say it exactly, it should take it as a non-constant lvalue reference. The following is the function template create in our first iteration.
If we compile the program, we will get a compiler error, due to the fact that the rvalue (line 21) cannot be bound to a non-constant lvalue reference.
Now, we have two ways to solve the issue.
- Change the non-constant lvalue reference (line 6) in a constant lvalue reference. We can bind an rvalue to a constant lvalue reference. But that is not perfect since the function argument is constant and we can therefore not change it.
- Overload the function template for a constant lvalue reference and a non-const lvalue reference. This is the easiest and better technique to implement.
Second Iteration
Here is the factory method create overloaded for a constant lvalue reference and a non-constant lvalue reference
The solution has two conceptional issues.
-
To support n different arguments, we must overload variations of the function template create. because the function
createwithout an argument is part of the perfect factory method. -
The function argument materializes in the function body of
createto an lvalue since it has a name.ais not movable anymore. Therefore, we have to perform an expensive copy rather than a cheap move. If the constructor ofT(line 12) needs an rvalue, it will not work anymore.
Now, we have the solution in the shape of the C++ function std::forward.
Third Iteration - std::forward
With std::forward, the solution looks promising:
Before we present the recipe from std::forward to get perfect forwarding, we will introduce the name universal reference.
The universal reference
(Arg&& a), in line 6, is a powerful reference that can bind lvalues or rvalues. Sometimes the term perfect forwarding reference is used for this special reference.
To achieve perfect forwarding, we have to combine a universal reference with std::forward. std::forward<Arg>(a) returns the underlying type of a, because a is a universal reference. We can think of std::forward as a conditional move operation. When the argument a is an rvalue, it moves its argument. When the argument a is an lvalue, it copies its argument. Therefore, an rvalue remains an rvalue.
Now to the pattern:
template<class T>
void wrapper(T&& a){
func(std::forward<T>(a));
}
We used this exact pattern in the function template create, but the name of the type has been changed from T to Arg in the example above.
Fourth Iteration - The Perfect Factory Method
Variadic Templates are templates that get an arbitrary number of arguments, which is exactly the feature missing from the perfect factory method.
The three dots, in lines 7 - 9, are the so-called parameter pack. If the three dots (also called ellipse) are on the left of Args, the parameter pack will be packed. If they are at the right of arg, the parameter pack will be unpacked. In case of the dots in line 3, the expression to the left is expanded, and a ‘,’ is put in between.
In particular, the three dots in line 9 std::forward<Args>(args)... causes each constructor call that performs perfect forwarding and the result is impressive. Now, we can invoke the perfect factory method without any arguments (line 40) or with three arguments (line 43).
The example in the next lesson will build on your understanding of this topic.