C++ Beyond the Syllabus #8: Move Semantics Pt. 4 — Forwarding References & std::forward

Plus, implementing `std::move` and `std::make_unique`

Jared Miller
8 min readAug 1, 2024

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:

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 type T. 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 reference Object 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 to make_unique with an rvalue will go to this overload, not the first overload. Then, using std::move to pass arg into the Object 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 type
  • ArgType 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&&. The typename 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 of T (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 of T 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 that std::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 type T 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 invoke std::forward like std::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

--

--

Jared Miller
Jared Miller

Written by Jared Miller

A C++ Software Engineer in high frequency trading. Excited about low-latency code, distributed systems, and education technology.

No responses yet