Modern C++ Features – templated rvalue References and std::forward

Contents

Combining rvalue references with templated function parameters or `auto` behaves quite differently than “normal” rvalue references. Together with the utility function template `std::forward` they allow something called “perfect forwarding” and are therefore also called forwarding references.

Reference collapsing

Consider a function template that takes what technically is an rvalue reference to its template parameter type as first argument:

template <class T> 
void zigzag(T&& t);

The C++11 has as set of rules stating what type `t` should have if `T` itself is of reference type. If `T` is an lvalue reference type, e.g. `X&` then `T&&` becomes `X& &&` which in turn is `X&`. If `T` is an rvalue type, e.g. `Y&&`, then `t` is of type `Y&& &&`, which is `Y&&`.

In other words, the type of `t` has the same rvalue/ravlue-ness as `T`. In addition, it also has the same const-ness and volatile-ness as `T`. So, if we let the compiler deduce the function parameter type, the type of `t` reflects exactly what we passed to the function:

struct X {};
X const fc() { return {}; }

int testCombinations()
{
    X x{};
    X const cx{};

    zigzag( x );      //T is X&
    zigzag( cx );     //T is X const&
    zigzag( X{} );    //T is X&& 
    zigzag( fc() );   //T is X const&& 
}

Perfect forwarding

So, what use has all this? In our function `zigzag`, `t` can be basically everything: It will be a reference, but the caller decides whether it will be an lvalue or lvalue reference. It can be const or not, and it can be volatile or not, or even both. It might be a const volatile rvalue reference – eww!

If we want to actually do something with this parameter, we should have an idea about it, and all that “may-or-may-not” fuzziness just induces headaches without giving a lot of benefits.

However, if we just want to pass the parameter on to some other function, we not only don’t care about what it is and what not. On the contrary, we often want to pass it exactly as we got it, without accidentally adding const or volatile and without stripping its rvalue-ness, if it is an rvalue. This is called perfect forwarding.

The const and volatile part is easy, if we don’t explicitly add it, we are good. However, since `t` has a name, it is unconditionally an lvalue. So, we should call `std::move` on it, but only if it is of rvalue reference type.

std::forward

This “conditional move” is achieved by another little utility template in the standard library, called `std::forward`. It is used like this:

template <class T> 
void zigzag(T&& t) {
  zap(std::forward<T>(t));
}

The call looks a bit clumsy, because we have to explicitly provide `T` as template parameter, we can not just call `std::forward(t)` like we can with `std::move`.

If we think about it a second, the reason becomes clear: As I wrote above, `t` is always an lvalue, because it has a name, and if we let the compiler deduce the template argument type of `std::forward`, it will not be able to figure out its rvalue-ness. So we have to explicitly provide `T`, because that type contains the information whether `std::forward` should restore the rvalue-ness or not.

What about auto?

Since the type deduction rules for `auto` variables are exactly the same as for function template parameters, all the above applies there, too. Since there is no `T`, we have to use `decltype`:

auto&& x = someFunction();
zap( std::forward<decltype(x)>(x) );

This looks even a little clumsier than forwarding a function parameter, but it’s still better than having a potentially long expression directly passed to the function. (If you are wondering what `decltype` is – that’s yet another new language feature I’ll cover in one of my future blog posts).

In addition, perfect forwarding of function parameters is much more common than with variables, and you’ll probably come across this little detail only when you write some very generic library code.

What are universal references?

The term “universal reference” is just another word for the same thing. It was popular before the term “forwarding reference” emerged. Syntactically it’s just a rvalue reference on a templated type, but I think you know by now that “forwarding reference” is a better fit. (If not, just read this proposal by Herb Sutter, Bjarne Stroustrup and Gabriel Dos Reis)

As always, don’t overuse it

Before you go ahead and perfectly forward everything everywhere, there is a caveat: Perfect forwarding function parameters only works on function templates.

Templates have to be implemented in the header, which in turn exposes the implementation to every translation unit that uses the header, and you might have to include additional headers, increasing the complexity of the header, compile time dependencies and build times.

In addition, as shown above, `std::forward` can be a bit of a clumsy read, so don’t impose reduced readability on the maintainers of your code, unless perfect forwarding really gives you needed benefits.

Carefully consider when and when not to use perfect forwarding.

Previous Post
Next Post

Leave a Reply

Your email address will not be published. Required fields are marked *