Perfect Forwarding and std::forward

Solving problems that arise when our functions forward their arguments to other functions
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

3D Character Concept Art
Ryan McCombe
Ryan McCombe
Posted

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:

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 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

Universal References

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

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 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

Perfect Forwarding using std::forward with Variadic Functions

One 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

Was this lesson useful?

Edit History

  • — First Published

Ryan McCombe
Ryan McCombe
Posted
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

7a.jpg
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access!

This course includes:

  • 106 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

C++ Iterators and Range Based For Loops

Learn how to iterate over containers in C++ using code that can work regardless of the specific type of container
124.jpg
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved