Perfect Forwarding

Perfect forwarding is an implementation concept which helps avoid the forwarding problem. Therefore, if we want to understand the need for perfect forwarding, we have to start by looking at the forwarding problem. This issue can occur when you have to write a generic function that takes references as parameters and forwards these parameters to another function.

 

The forwarding problem

An easy example of a template which forwards parameters to other functions is a factory. A factory provides a generic template function which is used to create instances of different objects. The generic function will call the constructor and maybe initializer of the object to create and therefore it forwards the factory function parameters to the object functions.

In the example of this article we want to create object instances for a class to manage and draw a line. Such a line is implemented by defining a start point, a length and a direction. To keep it simple we don’t want to implement the line classes itself with all details. We want to look at the creation of object instances and therefore we use an exemplary class definition which offers a constructor only. The combination of a point, length and direction to define the line allows us to use a data structure and a simple base type for the constructor parameters.

For the point data class we will use the following object:


struct Point

{

  Point()

  {

  };

  Point(int x, int y) : X{ x }, Y{ y }

  {

  };

private:

  double X;

  double Y;

};

We assume that we have different implementations for the line object. For example they may be platform-, device- or version specific. The following code shows two different line objects.


class Line

{

public:

  Line(Point& x, int direction, int length) 

    : mX{ x }, mDirection{ direction }, mLength{ length }

  {

  };

private:

  Point mX;

  int mDirection;

  int mLength;

};

class OtherLine

{

public:

  OtherLine(const Point& x, const int direction, const int length) 

    : mX{ x }, mDirection{ direction }, mLength{ length }

  {

  };

private:

  Point mX;

  int mDirection;

  int mLength;

};

As you can see the constructors have little differences: const vs. non const parameters. As we can’t change the implementations of the line classes, our factory has to support these different use cases.

 

As we now know the existing code, we can start to implement our factory. The following code shows a possible implementation. It contains a factory class with a template function to create the line instances. Furthermore we create a console application which will use the factory to create line instances. The factory is called several times, each time with a little bit different combination of parameters (members vs. temporary objects and integer literals).


class Factory

{

public:

  template<typename T, typename A, typename B, typename C>

  static T CreateModule(A x, B direction, C length)

  {

    return T(x, direction, length);

  }

};

int _tmain(int argc, _TCHAR* argv[])

{

  Point x{ 4, 7 };

  int i{ 6 };

  Factory::CreateModule<Line>(x, i, i);

  Factory::CreateModule<Line>(x, i, 5);

  Factory::CreateModule<Line>(x, 5, 5);

  Factory::CreateModule<Line>(Point(), i, i);

  Factory::CreateModule<Line>(Point(), i, 5);

  Factory::CreateModule<Line>(Point(), 5, 5);

  Factory::CreateModule<OtherLine>(x, i, i);

  Factory::CreateModule<OtherLine>(x, i, 5);

  Factory::CreateModule<OtherLine>(x, 5, 5);

  Factory::CreateModule<OtherLine>(Point(), i, i);

  Factory::CreateModule<OtherLine>(Point(), i, 5);

  Factory::CreateModule<OtherLine>(Point(), 5, 5);

  return 0;

}

So we are done. The factory is implemented and works fine for the different use cases. So where is the forwarding problem? I didn’t occur yet because we have implemented a factory which copies the function parameters and don’t forward it. Therefore the factory will work fine but you may get performance issues, especially if you have to use large data structures and not such simple ones like the point. Therefore, as next step, we want to forward the parameters instead of copy them. So we modify the factory function a little bit and pass the parameters as reference.


class Factory

{

public:

  template<typename T, typename A, typename B, typename C>

  static T CreateModule(A& x, B& direction, C& length)

  {

    return T(x, direction, length);

  }

};

int _tmain(int argc, _TCHAR* argv[])

{

  Point x{ 4, 7 };

  int i{ 6 };

  Factory::CreateModule<Line>(x, i, i);

  Factory::CreateModule<Line>(x, i, 5);         // error

  Factory::CreateModule<Line>(x, 5, 5);         // error

  Factory::CreateModule<Line>(Point(), i, i);

  Factory::CreateModule<Line>(Point(), i, 5);   // error

  Factory::CreateModule<Line>(Point(), 5, 5);   // error

  Factory::CreateModule<OtherLine>(x, i, i);

  Factory::CreateModule<OtherLine>(x, i, 5);    // error

  Factory::CreateModule<OtherLine>(x, 5, 5);    // error

  Factory::CreateModule<OtherLine>(Point(), i, i);

  Factory::CreateModule<OtherLine>(Point(), i, 5);  // error

  Factory::CreateModule<OtherLine>(Point(), 5, 5);  // error

  return 0;

}

Unfortunately this will result in errors. The function calls using rvalues will not work any longer. For example it is not possible to pass the integer literal as reference. But with another modification we may solve this issue. So let us try to change the factory function again and pass the parameters as const reference.


class Factory

{

public:

  template<typename T, typename A, typename B, typename C>

  static T CreateModule(const A& x, const B& direction, const C& length)

  {

    return T(x, direction, length);   // error

  }

};

int _tmain(int argc, _TCHAR* argv[])

{

  Point x{ 4, 7 };

  int i{ 6 };

  Factory::CreateModule<Line>(x, i, i);

  Factory::CreateModule<Line>(x, i, 5); 

  Factory::CreateModule<Line>(x, 5, 5); 

  Factory::CreateModule<Line>(Point(), i, i);

  Factory::CreateModule<Line>(Point(), i, 5);

  Factory::CreateModule<Line>(Point(), 5, 5);

  Factory::CreateModule<OtherLine>(x, i, i);

  Factory::CreateModule<OtherLine>(x, i, 5); 

  Factory::CreateModule<OtherLine>(x, 5, 5);   

  Factory::CreateModule<OtherLine>(Point(), i, i);

  Factory::CreateModule<OtherLine>(Point(), i, 5);  

  Factory::CreateModule<OtherLine>(Point(), 5, 5);  

  return 0;

}

Now the factory function call will work but the factory can no longer call the non const line constructors. One possible solution is overloading. You may provide overloaded versions of the factory function with const and non const parameters. Of course, with increasing number of parameters the number of possible parameter combinations will grow exponentially. And of course this procedure contradicts our goal to implement a single template function.

 

Now we see the forwarding problem and we can summarize it with the following explanation. The forwarding problem can occur when you write a generic function that takes references as its parameters forwards these parameters to another function. If the generic function takes a parameter of type T&, then the function cannot be called by using an rvalue. If the generic function takes a parameter of type const T&, then the called function cannot modify the value of that parameter.

 

Perfect forwarding

As we now have seen the forwarding problem we will try to solve this issue and implement perfect forwarding within our factory template function. This will be possible by using rvalue references parameters. They enable us to write one template function which accepts const and non const arguments and forwards them to another function as if the other function had been called directly. For more details about the concept of lvalues, rvalues and rvalue references you may read my previous article.

We can use the rvalue referenace declaratory and adapt our template function.


template<typename T, typename A, typename B, typename C>

static T CreateModule(A&& x, B&& direction, C&& length)

{

  ...

}

To understand this template we have to know two C++ rules: type deduction and reference collapsing.

 

Type deduction

In case of a template function T&& is not an rvalue reference. When the function is instantiated, T depends on whether the argument passed to the function is an lvalue or an rvalue. If it’s an lvalue of type U, T is deduced to U&. If it’s an rvalue, T is deduced to U. This rule may seem unusual but it starts making sense when we realize it was designed to solve the perfect forwarding problem.

 

Reference collapsing

The other rule is reference collapsing. Taking a reference to a reference is illegal in C++. However, it can sometimes arise in the context of templates and type deduction. On template instantiation there may be types like “int& &” or “int& &&”. While this is something which you cannot write in code the compiler will accept such template instantiations and infers a single reference from this. The reference collapsing rule is defined for this use case. The rule simply says that “&” always wins and the only case where “&&” results is “&& &&”. Following you will see all possible cases and the result after using the reference collapsing rule.

  • “& &” à “&”
  • “& &&” à “&”
  • “&& &” à “&”
  • “&& &&” à “&&”

 

Forwarding References or Universal References

As we have seen the two new rules we will now understand the rvalue reference within the deducing context. As this type of reference is different from a standard rvalue reference a new term was introduced: universal reference. This term was introduced by Scott Meyers because he clearly want to differentiate between true rvalue references and something what looks like an rvalue reference but might end up being an lvalue reference.

Later, several members of the C++ standard committee acknowledged the fact that there is a need to name the T&& references. So they came up with the term forwarding reference. The proposal for this change explains why the name forwarding reference is preferred over universal reference.

 

std::forward

After this short excursion we want to come back to our template function implementation. As we now understand forwarding references we know that they can be an rvalue or an lvalue reference. To forward this reference to a method the std::forward function was introduced. This function perfectly forwards each parameter either as an rvalue or as an lvalue, depending on how it was passed in.

So we can use the std::forward function to pass our parameters to the object constructor. Following you will see the final factory function of our example and the according use of the factory within the console application.


class Factory

{

public:

  template<typename T, typename A, typename B, typename C>

  static T CreateModule(A&& x, B&& direction, C&& length)

  {

    return T(std::forward<A>(x), std::forward<B>(direction), std::forward<C>(length));

  }

};

int _tmain(int argc, _TCHAR* argv[])

{

  Point x{ 4, 7 };

  int i{ 6 };

  Factory::CreateModule<Line>(x, i, i);

  Factory::CreateModule<Line>(x, i, 5);

  Factory::CreateModule<Line>(x, 5, 5);

  Factory::CreateModule<Line>(Point(), i, i);

  Factory::CreateModule<Line>(Point(), i, 5);

  Factory::CreateModule<Line>(Point(), 5, 5);

  Factory::CreateModule<OtherLine>(x, i, i);

  Factory::CreateModule<OtherLine>(x, i, 5);

  Factory::CreateModule<OtherLine>(x, 5, 5);

  Factory::CreateModule<OtherLine>(Point(), i, i);

  Factory::CreateModule<OtherLine>(Point(), i, 5);

  Factory::CreateModule<OtherLine>(Point(), 5, 5);

  return 0;

}

Perfect forwarding within the C++ standard

The standard libraries make use of perfect forwarding. For example the vector object as well as other containers offer the emplace_back function as alternative to push_back. In case of push_back a temporary object instance is created. This means it is necessary to construct, move and destruct the temporary object. If you use emplace_back instead, the object will be created directly within the vector without the need of a temporary object. This is possible because the function parameter passed to emplace_back us passed by using perfect forwarding.

 

Summary

The concept of perfect forwarding allows you to write efficient code which is executed without the need to create, move and destruct temporary objects. Perfect forwarding is based on forwarding references which use the concepts of type deduction and reference collapsing. These concepts are part of the language and so you can easily write functions using perfect forwarding.

Werbung
Dieser Beitrag wurde unter C++ veröffentlicht. Setze ein Lesezeichen auf den Permalink.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit deinem WordPress.com-Konto. Abmelden /  Ändern )

Facebook-Foto

Du kommentierst mit deinem Facebook-Konto. Abmelden /  Ändern )

Verbinde mit %s