C++ Beyond the Syllabus #8: Move Semantics Pt. 4 — Forwarding References & std::forward
Plus, implementing `std::move
` and `std::make_unique
`
Not a Medium member? View this entire article for free, here.
This article is a part of the C++ Beyond the Syllabus series. Subscribe here to receive each new issue directly in your inbox.
What should you do when you want two functions to have the same code, but the argument passed in can be an lvalue or an rvalue?
Enter: Forwarding References
In this article, I’ll cover what a forwarding reference is along with its partner in crime, std::forward
.
Together, these tools are used in a C++ idiom called perfect forwarding.
Learn These First
If you aren’t already familiar with lvalue references, rvalue references, and std::move
, I would definitely take a look at the previous few articles in this series.
Here are “friend links” (they won’t count toward your free article limit on Medium) to those articles:
- Move Semantics Pt. 1 — Lvalues, Rvalues & A Case For Shallow Copies
- Move Semantics Pt. 2 — Rvalue References & The Rule of Five
- Move Semantics Pt. 3 — std::move Explained Simply
A strong foundation in these topics is important for truly understanding perfect forwarding. Without this, you may very well find yourself making mistakes and not even realizing it.
Now, assuming you have read the previous articles, understanding perfect forwarding probably won’t be too hard. Let’s get to it!
Short & Sweet Definitions
A forwarding reference (or universal reference) is denoted as
T&&
for some template typeT
. This reference generally takes the form of whatever reference type (and constness) it is initialized with.
Here are some comparisons between forwarding references and rvalue references:
- An rvalue reference is also represented with two ampersands, but an rvalue reference requires a non-template type, while a forwarding reference is always templated.
- Like rvalue references, forwarding references have a name, so while the reference can bind to an lvalue or rvalue, the type of the forwarding reference itself is an lvalue.
std::forward
is a function that accepts a forwarding reference as a parameter and conditionally casts it to an rvalue, if and only if the forwarding reference is bound to an rvalue.
Used in conjunction, a forwarding reference and
std::forward
can eliminate the need for function overloads that differ only by the lvalue/rvalue-type of their parameter(s). This concept is commonly referred to as perfect forwarding.
Unless you are writing libraries or other heavily templated code, the above explanations might suffice. If you are templating heavily, want a more holistic understanding, or want to see how any of std::move
, std::forward
, or std::make_unique
are implemented… then read on!
Perfect Forwarding
There are a handful of use cases for functions overloaded to accept either an lvalue reference or an rvalue reference parameter.
For starters, the Rule of Five states that every object with dynamically allocated resources should implement an overloaded copy constructor and copy-assignment operator. Another example is function std::vector::push_back
, which has overloaded implementations for with either an lvalue reference or an rvalue reference parameter.
Now, let’s consider a third example, std::make_unique
, which I introduced in the article RAII & Smart Pointers. Let’s forget this is a templated function for a moment and write up a naive implementation specifically for an object, Object
, that accepts a single argument of type ArgType
:
unique_ptr<Object> make_unique( const ArgType& arg )
{
return new Object( arg );
}
unique_ptr<Object> make_unique( ArgType&& arg )
{
return new Object( std::move( arg ) );
}
Let’s walk through this:
- The first overload accepts a const lvalue reference. This means the function can bind to any lvalue, lvalue reference, rvalue, or rvalue reference for some argument of type
ArgType
. The argument is then passed into the const lvalue referenceObject
constructor overload. - The second
make_unique
overload accepts an rvalue reference. From our overload resolution priorities (discussed here & shown in chart below), we know any calls tomake_unique
with an rvalue will go to this overload, not the first overload. Then, usingstd::move
to passarg
into theObject
constructor preserves the rvalueness of the argument and invokes an rvalue reference constructor overload (or const lvalue reference overload, if the former is not defined).
This simplified example works, but we made quite a few assumptions and it’s not very realistic. So what changes would we have to make if we wanted std::make_unique
to work as a library function that can be templated on any type?
Here’s a few of the necessary changes:
Object
needs replaced with a template typeArgType
needs replaced with a variadic template parameter pack
In short, a variadic template parameter pack is a template type that encompasses some variable number of template arguments that can be of the same or different types. Parameter packs are represented by ellipses (…
) throughout C++.
Alright, so what does that look like?
Here is the make_unique
function stub from the C++ docs:
template< class T, class… Args >
unique_ptr<T> make_unique( Args&&… args );
If we were to modify our attempted implementation to resemble this use of parameter packs, we’d get the following:
template< class T, class… Args >
unique_ptr<T> make_unique( const Args& arg… )
{
return new T( arg… );
}
template< class T, class… Args >
std::unique_ptr<T> make_unique( Args&&… args )
{
return new T( std::forward<Args>( args )… );
}
So of the two overloads above, the latter is definitely more interesting. But first, do we even need the first overload?
The second overload accepts a parameter pack of universal references, meaning it accepts any number of any arguments of any object type(s) of any value type (lvalue/rvalue) of any constness. Soooo, that covers all of the bases that the first overload covers?
Yep — let’s get rid of the first overload.
This leaves us with the following implementation of std::make_unique
:
template< class T, class… Args >
std::unique_ptr<T> make_unique( Args&&… args )
{
return new T( std::forward<Args>( args )… );
}
The ellipses after the std::forward
function call is to “unpack” the parameter pack. It essentially will expand to a comma separated list of the preceding expression applied to each value in the pack. In this case, the unpacked expression will evaluate to:
std::forward<Arg1Type>( arg1 ), std::forward<Arg1Type>( arg2 ), // and so on
Now that we’ve covered forwarding references in depth, we can add it to our handy reference-binding chart from the previous articles:
One interesting observation here is that in overload resolution (at compile time), the forwarding reference overloads have lower priority than any overload with an explicit type/constness match, but higher priority than any other viable overload.
Though in practice, if you write a function accepting a forwarding reference, you shouldn’t really need any other function overloads.
Luckily for us, there’s a couple more use cases of forwarding references that we’ve already discussed in depth, which would be quite interesting to look at…
Implementing `std::move
`
The implementation of std::move
follows:
#include <type_traits>
namespace std
{
template<class T>
typename remove_reference<T>::type&& move(T&& a) noexcept
{
typedef typename remove_reference<T>::type&& RvalRef;
return static_cast<RvalRef>(a);
}
} // namespace std
There’s some new syntax in here, so let’s break it down.
- Value category (lvalue/rvalue) and constness aside, we’re working with a template type
T
. - The parameter of this function,
T&& a
, is a forwarding reference because it is denoted by a double ampersand following a template type. - The return value type is
typename remove_reference<T>::type&&
. Thetypename
keyword here just tells the compiler that it should expect the following expression to describe a type. - This return type also appears in the function body following the
typedef
keyword. This keyword just allows us to refer to the rather complex type name as its alias,RvalRef
, for the remainder of the scope. - The
remove_reference<T>::type
portion of our return value type simply says we should ignore the value category (lvalue/rvalue) and reference type and just deduce the underlying type ofT
(eg.int
,std::string
,Object
, etc.). This deduced type is no longer a template. We can’t see the type when looking at the code, but to the compiler, it is now some concrete type. Adding the&&
on the end of the deduced type ofT
makes the type an rvalue reference (recall, double ampersands following non-template types represent rvalue references). noexcept
can be ignored for now if you aren’t already familiar. The TLDR is that it assures the compiler a function will never throw any exceptions and can be optimized a little extra. See the C++ docs for more info.- The only line with side effects in the entire function body is the
static_cast
of our forwarding reference parameter to the rvalue reference type, which is returned from the function. (And this line is still evaluated at compile time!)
Hopefully that wasn’t too tricky to follow along with… and maybe now you see why I suggested std::move
actually be called std::cast_to_rvalue
… literally all it does is unconditionally cast its argument to an rvalue
.
Implementing `std::forward`
std::forward
also uses forwarding references and is pretty interesting to look at. Here’s one possible implementation:
#include <type_traits>
namespace std
{
template <typename T>
T&& forward(typename std::remove_reference<T>::type& t) noexcept
{
return static_cast<T&&>(t);
}
template <typename T>
T&& forward(typename std::remove_reference<T>::type&& t) noexcept
{
static_assert(!std::is_lvalue_reference<T>::value
, "Cannot forward an rvalue as an lvalue.");
return static_cast<T&&>(t);
}
} // namespace std
Let’s break this down:
- Recall from our walkthrough of
std::move
thatstd::remove_reference<T>::type
deduces a concrete, non-template type. Given that, we can see the first overload accepts an lvalue reference and the second overload accepts an rvalue reference (i.e. — neither of these overloads accept forwarding references as parameters). - We see that the only non-assertion line in each overload’s function body is exactly the same! They simply cast the lvalue or rvalue reference parameter to a forwarding reference (because
T
is a template type), which is now bound to either an lvalue or rvalue respectively (based on what overload we’re working with). - The
static_assert
in the second overload checks the condition!std::is_lvalue_reference<T>::value
, which will throw a compiler error if the template typeT
is actually an lvalue reference. You might be wondering why this assert is needed, “wouldn’t lvalue reference types just invoke the first overload?” Usually, yes. But, you could invokestd::forward
likestd::forward<rvalue_type>(lvalue)
, which would fail this assertion. You likely won’t run into this case in practice and if you do, the compiler will keep you in check.
Interesting that after seeing the implementation, it almost looks like std::forward
provides an overloaded library function we can use just so we don’t have to duplicate the code we write when overloads would differ only by their parameters’ lvalue/rvalue type.
And yea… that is basically the purpose.
What’s Next?
You now know enough to understand and use forwarding references in your code!
If you found this post helpful, please consider clapping, following, and sharing with your peers :)
Sources & More Info
- Effective Modern C++ by Scott Meyers, Items 23–25, 28
- “C++ Rvalue References Explained” by Thomas Becker
- “Understanding lvalues and rvalues in C and C++” by Eli Bendersky