0%

Perfect Forwarding in C++

This is my learning notes from the course - Mastering C++ Standard Library Features.

Perfect Forwarding

  • Forwarding references
  • std::forward

When no template argument deduction is happening, & means “lvalue reference“ and && means “rvalue reference“.

1
2
void take_lvalue(int&);
void take_rvalue(int&&);

In the context of template argument deduction, && has a different meaning: “forwarding reference“.

1
2
template <typename T>
void take_anything(T&&);

In the function signature above, T&& does NOT mean rvalue reference. Instead, it means “forwarding reference“.

A “forwarding reference“ binds to everything and remembers the original value category of the passed object. Here’s an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template <typename T>
void take_anything(T&&);

void example()
{
take_anything(0);
// `0` is a prvalue, which is an rvalue. Inside the body of `take_anything`:
// * `T` evaluates to `int`
// * `T&&` evaluates to `int&&`

int x;
take_anything(x);
// `x` is an lvalue. Inside the body of `take_anything`:
// * `T` evaluates to `int&`
// * `T&&` evaluates to `int&`

// Reference:
// http://en.cppreference.com/w/cpp/language/template_argument_deduction#Deduction_from_a_function_call
}

There is a special case that makes T evaluate to an lvalue reference when an lvalue is passed. This was intentionally added to the C++11 standard to allow “Perfect Forwarding“.

Note:
Remember that T&& is a forwarding reference only when T is being deduced.

In the below case, T is not being deduced, so T&& is an rvalue reference to T.

1
2
3
4
5
6
7
8
9
template <typename T>
struct foo
{
void not_a_forwarding_reference(T&&);
};

int main()
{
}

Why are forwarding references useful?

They “remember” the original value category of the passed object. This allows developers to write more generic code, avoiding repetition and providing more flexible and optimal interfaces.

Here’s an example. Let’s say we are writing a class that represents a vector of words. What we want to do is to provide a generic add() member function that accepts an std::string and moves it inside _words when possible.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <utility>
#include <vector>
#include <string>

class dictionary
{
public:
// ??? // provide an add() member function

private:
std::vector<std::string> _words;
};

int main()
{
dictionary d;

std::string s{"hello"};
d.add(s); // should copy

d.add(std::string{"world"}); // should move
}

One way of doing it is to provide two overloads as shown below. This works as intended, but causes code repetition as we discussed here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class dictionary
{
public:
// called if lvalue is passed
void add(const std::string& s)
{
_words.push_back(s); // copy to the vector
}

// called if rvalue is passed
void add(std::string&& s)
{
_words.push_back(std::move(s)); // move to the vector
}

private:
std::vector<std::string> _words;
};

Another solution is “pass-by-value and move“ idiom as shown below. This works, but it is not optimal. An extra move is executed on every invocation of add() as we discussed here.

1
2
3
4
5
6
7
8
9
10
11
class dictionary
{
public:
void add(std::string s)
{
_words.push_back(std::move(s));
}

private:
std::vector<std::string> _words;
};

An extra move is usually not a problem, but if you’re writing a generic library and you don’t know how your user is going to use it, then you want to be as optimal as possible in order to support a variety of use cases.

Therefore, this is a perfect chance to show how perfect forwarding and std::forward come into play.

1
2
3
4
5
6
7
8
9
10
11
12
class dictionary
{
public:
template <typename T>
void add(T&& s)
{
_words.push_back(std::forward<T>(s));
}

private:
std::vector<std::string> _words;
};

As you can see, if we take s as a forwarding reference, we can then “forward“ it to push_back in order to achieve optimal behavior without code repetition.

So, if the user passes an rvalue to add(), std::forward of T will detect that T is a temporary and will move s into vector. If an lvalue is passed as s, then std::forward will basically be a No-op and just copy s to the vector.

This works and it is now optimal. The only drawback is that add() is now an unconstrained template, and might need constraints like enable_if to make it play nicely with overload resolution and produce better compiler errors.

How does std::forward work?

You can think of std::forward as a “conditional move”. When the passed expression is an lvalue, std::forward will return an lvalue reference. When the passed expression is an rvalue, std::forward will return an rvalue reference.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <utility>
#include <vector>
#include <string>

void sink(int&); // (0)
void sink(int&&); // (1)

template <typename T>
void pipe(T&& x)
{
sink(std::forward<T>(x));

// Reference:
// http://en.cppreference.com/w/cpp/utility/forward
}

As you can see, we need to explicitly pass the T template parameter to std::forward. Since T evaluates to T& for lvalues with forwarding reference, it is the only way we can “informstd::forward that the original value category of x was an lvalue.

Summary

  • Forwarding references bind to everything and retain the original value category of the passed object.
  • Passing an lvalue to a forwarding reference makes the type T evaluate to T&.
  • std::forward must be used to correctly propagate the value category before any reference.
  • Perfect forwarding is a technique that allows you to provide optimal generic interfaces without having to repeat code.