22.7 Function Object Composition

Ru-Brd

Let's assume we have the following two simple mathematical functors in our framework:

  // functors/math1.hpp  #include <cmath>  #include <cstdlib>  class Abs {    public:  // ''function call'':  double operator() (double v) const {          return std::abs(v);      }  };  class Sine {    public:  // ''function call'':  double operator() (double a) const {          return std::sin(a);      }  }; 

However, the functor we really want is the one that computes the absolute value of the sine of a given angle. Writing the new functor is not hard:

 class AbsSine {    public:      double operator() (double a) {          return std::abs(std::sin(a));      }  }; 

Nevertheless, it is inconvenient to write new declarations for every new combination of functors. Instead, we may prefer to write a functor utility that composes two other functors. In this section we develop some templates that enable us to do this. Along the way, we introduce various concepts that prove useful in the remainder of this chapter.

22.7.1 Simple Composition

Let's start with a first cut at an implementation of a composition tool:

  // functors/compose1.hpp  template <typename FO1, typename FO2>  class Composer {    private:      FO1 fo1;  // first/inner function object to call  FO2 fo2;  // second/outer function object to call  public:  // constructor: initialize function objects  Composer (FO1 f1, FO2 f2)       : fo1(f1), fo2(f2) {      }  // ''function call'': nested call of function objects  double operator() (double v) {          return fo2(fo1(v));      }  }; 

Note that when describing the composition of two functions, the function that is applied first is listed first. This means that the notation Composer<Abs, Sine> corresponds to the function sin ( abs ( x )) (note the reversal of order). To test our little template, we can use the following program:

  // functors/compose1.cpp  #include <iostream>  #include "math1.hpp"  #include "compose1.hpp"  template<typename FO>  void print_values (FO fo)  {      for (int i=-2; i<3; ++i) {          std::cout << "f(" << i*0.1                    << ") = " << fo(i*0.1)                    << "\n";      }  }  int main()  {  // print  sin(abs(-0.5))      std::cout << Composer<Abs,Sine>(Abs(),Sine())(0.5) << "\n\n";  // print  abs()  of some values  print_values(Abs());      std::cout << '\n';  // print  sin()  of some values  print_values(Sine());      std::cout << '\n';  // print  sin(abs())  of some values  print_values(Composer<Abs, Sine>(Abs(), Sine()));      std::cout << '\n';  // print  abs(sin())  of some values  print_values(Composer<Sine, Abs>(Sine(), Abs()));  } 

This demonstrates the general principle, but there is room for various improvements.

A usability improvement is achieved by introducing a small inline helper function so that the template arguments for Composer may be deduced (by now, this is a rather common technique):

  // functors/composeconv.hpp  template <typename FO1, typename FO2>  inline  Composer<FO1,FO2> compose (FO1 f1, FO2 f2) {      return Composer<FO1,FO2> (f1, f2);  } 

With this in place, our sample program can now be rewritten as follows :

  // functors/compose2.cpp  #include <iostream>  #include "math1.hpp"  #include "compose1.hpp"  #include "composeconv.hpp"  template<typename FO>  void print_values (FO fo)  {      for (int i=-2; i<3; ++i) {          std::cout << "f(" << i*0.1                    << ") = " << fo(i*0.1)                    << "\n";      }  }  int main()  {  // print  sin(abs(-0.5))      std::cout << compose(Abs(),Sine())(0.5) << "\n\n";  // print  abs()  of some values  print_values(Abs());      std::cout << '\n';  // print  sin()  of some values  print_values(Sine());      std::cout << '\n';  // print  sin(abs())  of some values  print_values(compose(Abs(),Sine()));      std::cout << '\n';  // print  abs(sin())  of some values  print_values(compose(Sine(),Abs()));  } 

Instead of

 Composer<Abs, Sine>(Abs(), Sine()) 

we can now use the more concise

 compose(Abs(), Sine()) 

The next refinement is driven by a desire to optimize the Composer class template itself. More specifically , we want to avoid having to allocate any space for the members functors first and second if these functors are themselves empty classes (that is, when they are stateless ), which is a common special case. This may seem to be a modest savings in storage, but remember that empty classes can undergo a special optimization when passed as function call parameters. The standard technique for our purpose is the empty base class optimization (see Section 16.2 on page 289), which turns the members into base classes:

  // functors/compose3.hpp  template <typename FO1, typename FO2>  class Composer : private FO1, private FO2 {    public:  // constructor: initialize function objects  Composer(FO1 f1, FO2 f2)        : FO1(f1), FO2(f2) {      }  // ''function call'': nested call of function objects  double operator() (double v) {          return FO2::operator()(FO1::operator()(v));      }  }; 

This approach, however, is not really commendable. It prevents us from composing a function with itself. Indeed, the call of

  // print  sin(sin())  of some values  print_values(compose(Sine(),Sine()));  // ERROR: duplicate base class name  

leads to the instantiation of Composer such that it derives twice from class Sine , which is invalid.

This duplicate base problem can be easily avoided by adding an additional level of inheritance:

  // functors/compose4.hpp  template <typename C, int N>  class BaseMem : public C {    public:      BaseMem(C& c) : C(c) { }      BaseMem(C const& c) : C(c) { }  };  template <typename FO1, typename FO2>  class Composer : private BaseMem<FO1,1>,                   private BaseMem<FO2,2> {    public:  // constructor: initialize function objects  Composer(FO1 f1, FO2 f2)       : BaseMem<FO1,1>(f1), BaseMem<FO2,2>(f2) {      }  // ''function call'': nested call of function objects  double operator() (double v) {          return BaseMem<FO2,2>::operator()                   (BaseMem<FO1,1>::operator()(v));      }  }; 

Clearly, the latter implementation is messier than the original, but this may be an acceptable cost if it helps an optimizer realize that the resulting functor is "empty."

Interestingly, the function call operator can be declared virtual. Doing so in a functor that participates in a composition makes the function call operator of the resulting Composer object virtual too. This can lead to some strange results. We will therefore assume that the function call operator is nonvirtual in the remainder of this section.

22.7.2 Mixed Type Composition

A more crucial improvement to the simple Composer template is to allow for more flexibility in the types involved. With the previous implementation, we allow only functors that take a double value and return another double value. Life would be more elegant if we could compose any matching type of functor. For example, we should be able to compose a functor that takes an int and returns a bool with one that takes a bool and returns a double . This is a situation in which our decision to require member typedefs in functor types comes in handy.

With the conventions assumed by our framework, the composition template can be rewritten as follows:

  // functors/compose5.hpp  #include "forwardparam.hpp"  template <typename C, int N>  class BaseMem : public C {    public:      BaseMem(C& c) : C(c) { }      BaseMem(C const& c) : C(c) { }  };  template <typename FO1, typename FO2>  class Composer : private BaseMem<FO1,1>,                   private BaseMem<FO2,2> {    public:  // to let it fit in our framework:  enum { NumParams = FO1::NumParams };      typedef typename FO2::ReturnT ReturnT;      typedef typename FO1::Param1T Param1T;  // constructor: initialize function objects  Composer(FO1 f1, FO2 f2)       : BaseMem<FO1,1>(f1), BaseMem<FO2,2>(f2) {      }  // ''function call'': nested call of function objects  ReturnT operator() (typename ForwardParamT<Param1T>::Type v) {          return BaseMem<FO2,2>::operator()                   (BaseMem<FO1,1>::operator()(v));      }  }; 

We reused the ForwardParamT template (seeSection 22.6.3 on page 440) to avoid unnecessary copies of functor call arguments.

To use the composition template with our Abs and Sine functors, they have to be rewritten to include the appropriate type information. This is done as follows:

  // functors/math2.hpp  #include <cmath>  #include <cstdlib>  class Abs {    public:  // to fit in the framework:  enum { NumParams = 1 };      typedef double ReturnT;      typedef double Param1T;  // ''function call'':  double operator() (double v) const {          return std::abs(v);      }  };  class Sine {    public:  // to fit in the framework:  enum { NumParams = 1 };      typedef double ReturnT;      typedef double Param1T;  // ''function call'':  double operator() (double a) const {          return std::sin(a);      }  }; 

Alternatively, we can implement Abs and Sine as templates:

  // functors/math3.hpp  #include <cmath>  #include <cstdlib>  template <typename T>  class Abs {    public:  // to fit in the framework:  enum { NumParams = 1 };      typedef T ReturnT;      typedef T Param1T;  // ''function call'':  T operator() (T v) const {          return std::abs(v);      }  };  template <typename T>  class Sine {    public:  // to fit in the framework:  enum { NumParams = 1 };      typedef T ReturnT;      typedef T Param1T;  // ''function call'':  T operator() (T a) const {          return std::sin(a);      }  }; 

With the latter approach, using these functors requires the argument types to be provided explicitly as template arguments. The following adaptation of our sample use illustrates the slightly more cumbersome syntax:

  // functors/compose5.cpp  #include <iostream>  #include "math3.hpp"  #include "compose5.hpp"  #include "composeconv.hpp"  template<typename FO>  void print_values (FO fo)  {      for (int i=-2; i<3; ++i) {          std::cout << "f(" << i*0.1                    << ") = " << fo(i*0.1)                    << "\n";      }  }  int main()  {  // print  sin(abs(-0.5))      std::cout << compose(Abs<double>(),Sine<double>())(0.5)                << "\n\n";  // print  abs()  of some values  print_values(Abs<double>());  std::cout << '\n';  // print  sin()  of some values  print_values(Sine<double>());  std::cout << '\n';  // print  sin(abs())  of some values  print_values(compose(Abs<double>(),Sine<double>()));  std::cout << '\n';  // print  abs(sin())  of some values  print_values(compose(Sine<double>(),Abs<double>()));  std::cout << '\n';  // print  sin(sin())  of some values  print_values(compose(Sine<double>(),Sine<double>()));  } 

22.7.3 Reducing the Number of Parameters

So far we have looked at a simple form of functor composition where one functor takes one argument, and that argument is another functor invocation which itself has one parameter. Clearly, functors can have multiple arguments, and therefore it is useful to allow for the composition of functors with multiple parameters. In this section we discuss the implication of allowing the first argument of Composer to be a functor with multiple parameters.

If the first functor argument of Composer takes multiple arguments, the resulting Composer class must accept multiple arguments too. This means that we have to define multiple Param N T member types and we need to provide a function call operator (operator () ) with the appropriate number of parameters. The latter problem is not as hard to solve as it may seem. Function call operators can be overloaded; hence we can just provide function call operators for every number of parameters up to a reasonably high number (an industrial-strength functor library may go as high as 20 parameters). Any attempt to call an overloaded operator with a number of parameters that does not match the number of parameters of the first composed functor results in a translation (compilation) error, which is perfectly all right. The code might look as follows:

 template <typename FO1, typename FO2>  class Composer : private BaseMem<FO1,1>,                   private BaseMem<FO2,2> {    public:    // ''function call'' for no arguments:  ReturnT operator() () {          return BaseMem<FO2,2>::operator()                   (BaseMem<FO1,1>::operator()());      }  // ''function call'' for one argument:  ReturnT operator() (typename ForwardParamT<Param1T>::Type v1) {          return BaseMem<FO2,2>::operator()                   (BaseMem<FO1,1>::operator()(v1));      }  // ''function call'' for two arguments:  ReturnT operator() (typename ForwardParamT<Param1T>::Type v1,                          typename ForwardParamT<Param2T>::Type v2) {          return BaseMem<FO2,2>::operator()                   (BaseMem<FO1,1>::operator()(v1, v2));      }   }; 

We are now left with the task of defining members Param1T , Param2T , and so on. This task is made more complicated by the fact that these types are used in the declaration of the various function call operators: These must be valid even though the composed functors do not have corresponding parameters. [9] For example, if we compose two single-parameter functors, we must still come up with a Param2T type that makes a valid parameter type. Preferably, this type should not accidentally match another type used in a client program. Fortunately, we already solved this problem with FunctorParam template. The Compose template can therefore be equipped with its various member typedefs as follows:

[9] Note that the SFINAE principle (see Section 8.3.1 on page 106) does not apply here because these are ordinary member functions and not member function templates. SFINAE is based on template parameter deduction , which does not occur for ordinary member functions.

 template <typename FO1, typename FO2>  class Composer : private BaseMem<FO1,1>,                   private BaseMem<FO2,2> {    public:  // the return type is straightforward:  typedef typename FO2::ReturnT ReturnT;  // define  Param1T  ,  Param2T  , and so on   // - use a macro to ease the replication of the parameter type construct  #define ComposeParamT(N) \          typedef typename FunctorParam<FO1, N>::Type Param##N##T      ComposeParamT(1);      ComposeParamT(2);   ComposeParamT(20);  #undef ComposeParamT   }; 

Finally, we need to add the Composer constructors. They take the two functors being composed, but we allow for the various combinations of const and non- const functors:

 template <typename FO1, typename FO2>  class Composer : private BaseMem<FO1,1>,                   private BaseMem<FO2,2> {    public:    // constructors:  Composer(FO1 const& f1, FO2 const& f2)       : BaseMem<FO1,1>(f1), BaseMem<FO2,2>(f2) {      }      Composer(FO1 const& f1, FO2& f2)       : BaseMem<FO1,1>(f1), BaseMem<FO2,2>(f2) {      }      Composer(FO1& f1, FO2 const& f2)       : BaseMem<FO1,1>(f1), BaseMem<FO2,2>(f2) {      }      Composer(FO1& f1, FO2& f2)       : BaseMem<FO1,1>(f1), BaseMem<FO2,2>(f2) {      }   }; 

With all this library code in place, a program can now use simple constructs, as illustrated in the following example:

  // functors/compose6.cpp  #include <iostream>  #include "funcptr.hpp"  #include "compose6.hpp"  #include "composeconv.hpp"  double add(double a, double b)  {      return a+b;  }  double twice(double a)  {      return 2*a;  }  int main()  {      std::cout << "compute (20+7)*2: "                << compose(func_ptr(add),func_ptr(twice))(20,7)                << '\n';  } 

These tools can still be refined in various ways. For example, it is useful to extend the compose template to handle function pointers directly (making the use of func_ptr in our last example unnecessary). However, in the interest of brevity, we prefer to leave such improvements to the interested reader.

Ru-Brd


C++ Templates
C++ Templates: The Complete Guide
ISBN: 0201734842
EAN: 2147483647
Year: 2002
Pages: 185

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net