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
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
3D Character Concept Art
Ryan McCombe
Ryan McCombe
Updated

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

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

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.

Was this lesson useful?

Next Lesson

Iterators and Ranges

This lesson offers an in-depth look at iterators and ranges, emphasizing their roles in container traversal
3D Character Concept Art
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

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

A computer programmer
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
Working with Functions
A computer programmer
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:

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

Iterators and Ranges

This lesson offers an in-depth look at iterators and ranges, emphasizing their roles in container traversal
3D Character Concept Art
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved