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 | void take_lvalue(int&); |
In the context of template argument deduction, &&
has a different meaning: “forwarding reference
“.
1 | template <typename 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 | template <typename T> |
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 | template <typename T> |
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 |
|
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 | class dictionary |
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 | class dictionary |
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 | class dictionary |
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 |
|
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 “inform“ std::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 typeT
evaluate toT&
. 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.