std::forward
In our earlier lesson on move semantics, we talked about how copying an object could be an expensive operation. This is particularly true if the object has many nested resources. For example, one of its class members could be an array storing 1,000 other objects.
Copying everything can be an expensive operation. It’s sometimes necessary - but not always. Sometimes, when we want to copy an object, we no longer care about the original object we’re copying from. This is because, after the copy operation, we’ll no longer need it.
This allows us to implement a faster operation where, instead of copying all the subresources to the new object, we simply move them by, for example, reallocating ownership of the array containing those subresources to the new object.
This means the original object will be in a degraded state, sometimes called the moved-from state. But this operation is for situations where we no longer need the original object, so that doesn’t matter - we can just take the performance gains.
Adding support for these two different forms of copying is referred to as implementing move semantics. What it actually involves depends on the nature of our class, and what resources it has. It is up to us, as the people writing our class, to decide if we want to implement move semantics and, if we do, how it should work.
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}
23
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 takes 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 move semantics, we’d want to implement that operator too. For this lesson, we’ll just implement the constructor, as our focus is on forwarding rather than move 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 have a 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 capture both lvalue and rvalue references. 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, that associated parameter 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 motivations we’ll have for writing variadic functions is to implement the intermediate 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
— First Published
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.