std::forward
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.
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. A more detailed guide to copy and move semantics is available earlier in the course:
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){
return T{Original};
}
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){
return T{Original};
}
This may be confusing, as we previously showed that &&
denotes an rvalue reference. However, when used with a template type (including the auto
keyword) it is a forwarding reference.
A forwarding reference can bind to both lvalues and rvalues, preserving their value category. Our previous example now compiles:
#include <iostream>
class Character {/*...*/};
template <typename T>
T Build(T&& Original){
return T{Original};
}
int main(){
Character A;
Character B{Build(A)};
Character C{Build(std::move(A))};
}
But we now have a second issue - 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
In the past, this style of reference was sometimes referred to as a universal reference, given it captures both lvalues and rvalues
More recently, these references were given their official name: forwarding references
The "universal reference" phrasing is still used, particularly in older resources, but they refer to the same concept
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 was the 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>
T Build(T&& Original){
return 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>
class Character {/*...*/};
template <typename T>
T Build(T&& Original){
return T{std::forward<T>(Original)};
}
int main(){
Character A;
Character B{Build(A)};
Character C{Build(std::move(A))};
}
Our basic Build()
function is now working correctly, complete with perfect forwarding:
Copying
Moving
std::forward()
with Variadic FunctionsOne of the most common use cases for variadic functions is to implement the forwarding behavior seen in functions like std::make_unique()
 and emplace()
.
The following example adapts our Build()
function into a variadic template, that accepts any number of arguments.
These arguments will ultimately be forwarded to a Character
constructor. But, without perfect forwarding, we’ve lost the benefits of move semantics:
#include <iostream>
class Character {/*...*/};
template <typename T, typename... Args>
T Build(Args&&... Arguments){
return 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))};
}
Constructing Legolas
Copying Legolas
Copying Legolas
We can update this example to use std::forward()
at the location where we’re expanding our parameter pack:
#include <iostream>
class Character {/*...*/};
template <typename T, typename... Args>
T Build(Args&&... Arguments){
return T{std::forward<Args>(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))};
}
Constructing Legolas
Copying Legolas
Moving Legolas
In this lesson, we've introduced perfect forwarding, and how std::forward()
can help us implement it. The key takeaways include:
std::forward()
to maintain the original value category of arguments, ensuring that our functions can perfectly forward arguments to other functions, preserving efficiency.std::forward()
in that context.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
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.