Modern C++ In-Depth — Perfect Forwarding
In our previous blog post, we took a look at how to use value categories and rvalue references to implement move semantics. In this post, we’ll take a look at another type of reference and how it can help us write more generic code.
Motivation
Let’s suppose it’s 2012. We’ve just upgraded to the latest C++11 conforming compiler and we’re excited to start using smart pointers. After playing around with std::make_shared<T>(...)
, we realize that there is no analogous std::make_unique<T>(...)
function – it was overlooked in C++11, but later added to C++14.
How might we go about writing our own version of std::make_unique<T>(...)
?
One of the primary goals of our new function is that users should not have to use the new
keyword anymore. That means that the arguments to our fds::make_unique<T>(...)
function should match T
's constructor arguments. After a bit of trial and error, we come up with the following set of overloads to cover types that take up to two constructor arguments:
namespace fds {
template <typename T, typename A>
std::unique_ptr<T> make_unique(const A& a)
{
return std::unique_ptr<T>(new T(a));
}template <typename T, typename A>
std::unique_ptr<T> make_unique(A& a)
{
return std::unique_ptr<T>(new T(a));
}template <typename T, typename A, typename B>
std::unique_ptr<T> make_unique(const A& a, const B& b)
{
return std::unique_ptr<T>(new T(a, b));
}template <typename T, typename A, typename B>
std::unique_ptr<T> make_unique(const A& a, B& b)
{
return std::unique_ptr<T>(new T(a, b));
}template <typename T, typename A, typename B>
std::unique_ptr<T> make_unique(A& a, const B& b)
{
return std::unique_ptr<T>(new T(a, b));
}template <typename T, typename A, typename B>
std::unique_ptr<T> make_unique(A& a, B& b)
{
return std::unique_ptr<T>(new T(a, b));
}
} // namespace fds
That’s a lot of overloads! In fact, the number of overloads grows geometrically with the number of parameters.
Let’s see how we can improve on this using forwarding references.
Forwarding References
In the previous post, we saw how the double ampersand lexeme can be used to declare rvalue references, allowing developers to declare variables that can bind to temporaries. You may have noticed that none of the examples in the previous post used that lexeme in combination with a template parameters. That’s because doing so actually declares another type of reference, known as a forwarding reference. (Note that forwarding references were also known as universal references in early educational content.)
While a forwarding reference may at first appear to be an rvalue reference, it actually has a very particular form that allows it to bind to any value category. The compiler is able to preserve the original value category for later use, allowing developers to “forward” variables to other function calls.
Before examining how we might use these forwarding references, however, let’s take a closer look at how to declare them.
void foo(std::string&& data); //< An rvalue reference.template <typename T>
void bar(T&& data); //< A forwarding reference.
In the examples above, the function foo(...)
takes as input an rvalue reference (to a std::string
). In contrast, the function bar(...)
takes a forwarding reference as input.
A forwarding reference always takes the form T&&
, where T
refers to a function template parameter. Using T&&
when T
is a class template will result in an rvalue reference instead of a forwarding reference, as demonstrated below:
template <typename T>
class Widget
{
public:
void foo(T&& data); //< Caution! Rvalue reference. template <typename Arg>
void bar(Arg&& data); //< This is a forwarding reference.
};
Lastly, it’s worth noting that a forwarding reference cannot be written as const T&&
either. As with rvalue references, forwarding references should not be const
. A const
rvalue reference would prevent move operations, and a const
forwarding reference would likewise prevent forwards.
Using std::forward<T>(...)
As alluded to earlier, forwarding references let us pass on, or “forward,” parameters from one function to another. To illustrate this, consider the following example:
template<typename T>
class Widget
{
public:
Widget(const T& data) : m_data(data)
{
std::cout << "lvalue\n";
} // Note that is an rvalue reference.
Widget(T&& data) : m_data(std::move(data))
{
std::cout << "rvalue\n";
} private:
T m_data;
};template<typename T, typename Arg>
Widget<T> factory(Arg&& arg)
{
return Widget<T>(std::forward<Arg>(arg));
}int main()
{
const std::string str = "An lvalue example";
const auto foo = factory<std::string>(str);
// ...prints "lvalue"
const auto bar = factory<std::string>("An rvalue example");
// ...prints "rvalue" return 0;
}
The formal parameter to factory(...)
is a forwarding reference. When constructing foo
, arg
is bound to an lvalue (i.e., str
), but when constructing bar
that same formal parameter is bound to an rvalue (i.e., the string literal "An rvalue example"
). In order to pass the exact types received by factory(...)
to the Widget
constructor, we'll need the help of the std::forward<T>(...)
utility function. The use of std::forward<T>(...)
in this context is very similar to that of std::move(...)
in our previous post.
The crucial difference between these two utility functions is that while std::move(...)
will unconditionally cast its argument to an rvalue reference, std::forward<T>(...)
will do so conditionally. Since forwarding references can bind to both lvalues and rvalues, std::forward<T>(...)
will only cast its argument to an rvalue if it receives an rvalue.
Improving fds::make_unique<T>(...)
Using forwarding references, we can now reduce our earlier overload growth from geometric to linear growth:
namespace fds {
template <typename T, typename A>
std::unique_ptr<T> make_unique(A&& a)
{
return std::unique_ptr<T>(new T(std::forward<A>(a)));
}template <typename T, typename A, typename B>
std::unique_ptr<T> make_unique(A&& a, B&& b)
{
return std::unique_ptr<T>(new T(std::forward<A>(a),
std::forward<B>(b)));
}
} // namespace fds
That’s a lot more manageable.
There is one additional trick that we can employ to reduce this linear growth to a single function template, but we’ll cover that at a later date.
Using std::move(...)
vs std::forward<T>(...)
Now that we’ve seen both of these utility functions in action, which one should we use?
std::move(...)
is the right tool for the job when we know that we're dealing with an rvalue, or if we wish to invoke a function that takes an rvalue reference. When we're dealing with a forwarding reference, which can bind to either an lvalue or an rvalue, we'll need to use std::forward<T>(...)
to preserve the original value category. If we were to use std::move(...)
with a forwarding reference, we risk potentially modifying an lvalue, which may have undesirable side-effects.
void serialize(const std::chrono::system_clock::time_point&,
std::string);
void send_response(const std::string&);template <typename T>
void log(T&& data)
{
// Since log(...) might be called with an lvalue, we cannot
// be sure that we are entitled to pass on resource
// ownership here. Thus, std::move(data) is incorrect.
serialize(std::chrono::system_clock()::now(),
std::move(data));
}void process_request(/*...*/)
{
std::string payload = compute_response(/*...*/);
log(payload); // `payload` is now in a moved-from state and likely to be
// empty as a result!
send_response(payload);
}
Using std::move(...)
indicates that resource ownership is being handed off, whereas std::forward<T>(...)
leaves open the possibility that the callee may take possession of the resource, depending on the exact value category that is being forwarded.
Forwarding References and auto
There is one more way to declare a forwarding reference, and that is by using auto&&
. Since forwarding references can bind to any value category, auto&&
can come in handy when writing generic code.
template <typename T>
void foo(T&& collection)
{
for (auto&& item : collection) {
// Since T&& could be almost anything, we can make our
// code more generic by allowing `item` to bind to
// whatever is contained within the `collection`, without
// regard for cv-qualification.
}
}
What’s Next?
In the next installment we’ll take a look at how we can turn our fds::make_unique(...)
example into a single function using a parameter pack.
Articles in this series
Modern C++ In-Depth — Move Semantics, Part 1
Modern C++ In-Depth — Move Semantics, Part 2
Acknowledgments
Special thanks to all that contributed to this blog post:
Author: Tim Severeijns
Reviewers: James Abbatiello, Michael Kristofik, Jens Maurer, Matt Topol