Freestyling on patterns, idioms and semantics…

Icon

vivid hallucinations for bloodthirsty digital vampires

All the things you have always wanted to know about writing a generic perfect forwarder

A generic forwarder is a template function supposed to forward the passed arguments to a target function or a callable object (provided for example as an extra argument).

To be perfect a forwarder must be able to be deployed in different contexts, allowing to pass both lvalue and rvalue parameters, yet having the template parameter types correctly deducted and the target function properly chosen from a possible overloaded set.

The current C++ standard does not allow to implement such a perfect forwarder that instead is enabled by means of C++0x rvalue reference. Nevertheless, a generic non-perfect but correct forwarded is possible with the help of tr1::reference_wrapper.

The most desirable feature for a generic wrapper is a mechanism for passing the arguments to a target function that accepts them in different ways: by copy, by reference and by const reference.

The client of the generic forwarder, indeed, must be able to pass either lvalue or rvalue arguments in accordance with the target’s signature: that is if a target parameter (the parameter of the target function) is taken by non-const reference, the corresponding parameter passed to the wrapper should be provided accordingly (as lvalue).

In this study a comparison of three different forwarders is presented, introducing the concept of perfect forwarding enabled by means of rvalue references.

The following  function has been chosen as target object (it could be a functor or any other callable object):

int fun(arg a1, const arg &a2, arg &a3)
{
    a3 = arg(1);
    ...
    return 0;
}

The first signature examined is the following one:

template <typename F, typename T1, typename T2, typename T3>
int candidate_forwarder_1(F &f, T1 &t1, T2 &t2, T3 &t3)
{
    return fun(t1,t2,t3);
}

All the arguments are passed by non-const reference.

The candidate_forwarder_1 sports no performance loss in that no additional copy constructors are called during the parameters passing (comparisons are provided by test #0 and #1). However such a wrapper has a flaw: The caller of this wrapper is not allowed to pass a temporary object although the target function was designed to accept it by value or by const reference (ie: the second parameter of function fun).

This weakness is a consequence of a rule imposed by the standard that does not allow a non-const reference to bind an rvalue. The rule is intended to prevent the modification of an rvalue (temporary object) that, prior to the understanding of the importance of the move and forward semantic of C++0x, did not make much sense.

To get out of this scrape a second candidate for the perfect forwarder is considered:

template <typename F, typename T1, typename T2, typename T3>
int candidate_forwarder_2(F f, T1 t1, T2 t2, T3 t3)
{
    return fun(t1,t2,t3);
}

In this wrapper all the arguments are passed by value, which means that it’s possible to pass both lvalue and rvalue. However there’s a double problem here: not only are the arguments copy-constructed, but also the type deduction decay now comes into play. The decay makes array and function types resolved as pointers, as well as the top-level CV qualifiers discarded (see template argument deduction decay: C++ Template: the complete guide [Vandevoorde/Josuttis]).

As a consequence this second candidate shows following problems:

  1. it’s not perfect due to the template argument deduction decay. The template argument types may be resolved as types that cannot be used to select the proper target function (overloading failure).
  2. parameters of the target function taken by reference are bound to the formal parameters of the forwarder (t3, in the example) and not to those provided by the forwarder client.
  3. parameters are passed by value and additional copy constructors are involved.

The test #2 shows that the issue 2 makes this wrapper incorrect:

the actual argument a3 passed to the wrapper is not updated by the target function as expected (the formal parameter t3 is updated in place of it).

Fortunately a workaround exists: the tr1::reference_wrapper class can be used to wrap the arguments supposed to be passed as non-const reference to the target function.

Hence if the instantiation point is like just this one:

candidate_forwarder_2(fun, a1, a2, std::tr1::ref(a3));

the wrapper produces the expected result. Test #3 and #4 show the correctness of the forwarder function.

Alas the comparison of the test #0 and #4 shows further copy constructors involved when passing the argument a1 and a2 (*3). This is the price to pay to implement a wrapper that can accept both lvalue and rvalue parameters.

It’s worth noting that when passing an rvalue object no additional copy constructor is involved thanks to the copy elision optimization implemented in most of the compilers available today. The test #3 shows such an optimization in action:

candidate_forwarder_2(fun, arg(), arg(), std::tr1::ref(a3));

In despite of the sub optimal performance pointed out by the test #4, the candidate_forwarder_2 wrapper is however preferable over the candidate_forwarder_1, in that it admits rvalues and temporaries as arguments.

Although not perfect (*1), it’s the most convenient implementation the current C++ standard allows to implement. The object adapter tr1::bind, for instance, is written exactly this way.

As final candidate, the perfect forwarder is presented. It is implemented by means of C++0x rvalue reference:

template <typename F, typename T1, typename T2, typename T3>
int the_perfect_forwarder(F f, T1 &&t1, T2 &&t2, T3 &&t3)
{
    return fun(std::forward<T1>(t1),
               std::forward<T2>(t2),
               std::forward<T3>(t3));
}

The limitation that a non-const reference cannot bind an rvalue is removed by means of the rvalue reference [&&] which is supposed to bind to both lvalues and rvalues when it’s a template parameter.
Bearing in mind the  reference collapsing rules introduced by C++0x :

  • T& -> & -> T&
  • T& -> && -> T&
  • T&& -> & -> T&
  • T&& -> && -> T&&

The last forwarder sports the following properties:

  • No additional copy constructors are involved during the argument passing.
  • Either a lvalue or an rvalue can be provided as argument, in accordance with the signature of the target function/object.
  • The template argument deduction for an rvalue reference is done in the following way:
    • if a lvalue of type A is passed, T resolves to A& and by the reference collapsing rules, the argument type becomes A& (lvalue reference);
    • if an rvalue is passed, T resolves to A, and the argument type becomes A&& (no collapsing rule is applied).

However the rvalue reference itself is not sufficient to implement a perfect forwarder because once the arguments are passed to the wrapper they have a name and therefore they all are l-values. To restore their original type the std::forward<> helper function can be used to implement the correct forward semantic:

namespace std
{
    template <typename T>
    && forward(T&& a)
    {
        return a;
    }
}

Why std::forward<> is required? Let’s do an in-depth examination:

  • If a lvalue of type A is passed to std::forward<> as argument, the corresponding type T of the wrapper resolves to A&. Then by the collapsing rules, the argument and the return type of std::forward become A&.
  • If a rvalue of type A is passed to std::forward<> as argument, the corresponding type T of the wrapper resolves to A. Then the argument and the return type of std::forward become A&&.

That said, the return type of std::forward<> depends on type explicitly specified in the template at the instantiation point (argument deduction is not used in the std::forward). If it’s not yet clear, the following rule puts more light on the problem:

Objects declared as rvalue reference can be either lvalues or rvalues. The distinguishing criterion to bear in mind is: if the instance has a name then it is a lvalue; an rvalue otherwise.

In accordance with this criterion it follows that:

The return type of std::forward is an expression that (since it has no-name) it is an rvalue reference when an rvalue object is passed as argument, or a lvalue reference otherwise.

The std::forward template function used in the perfect forwarder returns an expression whose type is exactly the same as that of the argument passed to the wrapper. This enables the perfect forwarding.

Ciao

Advertisements

Filed under: c++, C++11, programming, ,

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: