22.6 Introspection

Ru-Brd

In the context of programming, the term introspection refers to the ability of a program to inspect itself. For example, in Chapter 15 we designed templates that can inspect a type and determine what kind of type it is. For functors, it is often useful to be able to tell, for example, how many arguments the functor accepts, the return type of the functor, or the n th parameter type of the functor type.

Introspection is not easily achieved for an arbitrary functor. For example, how would we write a type function that evaluates to the type of the second parameter in a functor like the following?

 class SuperFunc {    public:      void operator() (int, char**);  }; 

Some C++ compilers provide a special type function known as typeof . It evaluates to the type of its argument expression (but doesn't actually evaluate the expression, much like the sizeof operator). With such an operator, the previous problem can be solved to a large extent, albeit not easily. The typeof concept is discussed in Section 13.8 on page 215.

Alternatively, we can develop a functor framework that requires participating functors to provide some extra information to enable some level of introspection. This is the approach we use in the remainder of this chapter.

22.6.1 Analyzing a Functor Type

In our framework, we handle only class type functors [7] and require them to provide the following information:

[7] To reduce the strength of this constraint, we also develop a tool to encapsulate function pointers in the framework.

  • The number of parameters of the functor (as a member enumerator constant NumParams )

  • The type of each parameter (through member typedefs Param1T , Param2T , Param3T ,...)

  • The return type of the functor (through a member typedef ReturnT )

For example, we could rewrite our PersonSortCriterion as follows to fit this framework:

 class PersonSortCriterion {    public:      enum { NumParams = 2 };      typedef bool ReturnT;      typedef Person const& Param1T;      typedef Person const& Param2T;      bool operator() (Person const& p1, Person const& p2) const {  // returns whether  p1  is ''less than''  p2   }  }; 

These conventions are sufficient for our purposes. They allow us to write templates to create new functors from existing ones (for example, through composition).

There are other properties of a functor that can be worth representing in this manner. For example, we could decide to encode the fact that a functor has no side effects and use this information to optimize certain generic templates. Such functors are sometimes called pure functors . It would also be useful to enable introspection of this property to enforce the need for a pure functor at compile time. For example, usually a sorting criterion should be pure [8] ; otherwise , the results of the sorting operation could be meaningless.

[8] At least to a large extent. Some caching and logging side effects can be tolerated to the extent that they don't affect the value returned by the functor.

22.6.2 Accessing Parameter Types

A functor can have an arbitrary number of parameters. With our conventions it is relatively straightforward to access, say, the eighth parameter type: Param8T . However, when dealing with templates it is always useful to plan for maximum flexibility. In this case, how do we write a type function that produces the N th parameter type given the functor type and a constant N ? We can do this by writing partial specializations of the following class template:

 template<typename FunctorType, int N>  class FunctorParam; 

We can provide partial specializations for values of N from one to some reasonably large number (say 20; functors rarely have more than 20 parameters). Each of these partial specializations can then define a member typedef Type that reflects the corresponding parameter type.

This presents one difficulty: To what should FunctorParam<F, N>::Type evaluate when N is larger than the number of parameters of the functor F ? One possibility is to let such situations result in a compilation error. Although this is easily accomplished, it makes the FunctorParam type function much less useful than it could be. A second possibility is to default to type void . The disadvantage of this approach is that there are some unfortunate restrictions on type void ; for example, a function cannot have a parameter type of type void , nor can we create references to void . Therefore, we opt for a third possibility: a private member class type. Objects of such a type are not easily constructed , but there are few syntactic constraints on their use. Here is an implementation of this idea:

  // functors/functorparam1.hpp  #include "ifthenelse.hpp"  template <typename F, int N>  class UsedFunctorParam;  template <typename F, int N>  class FunctorParam {    private:      class Unused {        private:          class Private {};        public:          typedef Private Type;      };    public:      typedef typename IfThenElse<F::NumParams>=N,                                  UsedFunctorParam<F,N>,                                  Unused>::ResultT::Type              Type;  };  template <typename F>  class UsedFunctorParam<F, 1> {    public:      typedef typename F::Param1T Type;  }; 

The IfThenElse template was introduced in Section 15.2.4 on page 272. Note that we introduced a helper template UsedFunctorParam , and it is this template that needs to be partially specialized for specific values of N . A concise way to do this is to use a macro:

  // functors/functorparam2.hpp  #define FunctorParamSpec(N)                           \          template<typename F>                          \          class UsedFunctorParam<F, N> {                \            public:                                     \              typedef typename F::Param##N##T Type;     \          }   FunctorParamSpec(2);  FunctorParamSpec(3);   FunctorParamSpec(20);  #undef FunctorParamSpec 

22.6.3 Encapsulating Function Pointers

Requiring that functor types support some introspection in the form of member typedefs excludes the use of function pointers in our framework. As discussed earlier, we can mitigate this limitation by encapsulating the function pointer. Let's develop a small tool that enables us to encapsulate functions with as many as two parameters (a larger number of parameters are handled in the same way, but let's keep the number small in the interest of clarity). We cover only the case of functions with C++ linkage; C linkage can be done in a similar way.

The solution presented here has two main components : a class template FunctionPtr with instances that are functor types encapsulating a function pointer, and an overloaded function template func_ptr() that takes a function pointer and returns a corresponding functor that fits our framework. The class template is parameterized with the return type and the parameter types:

 template<typename RT, typename P1 = void, typename P2 = void>  class FunctionPtr; 

Substituting a parameter with type void amounts to saying that the parameter isn't actually available. Hence, our template is able to handle multiple numbers of functor call arguments.

Because we need to encapsulate a function pointer, we need a tool to create the type of the function pointer from the parameter types. This is achieved through partial specialization as follows:

  // functors/functionptrt.hpp   // primary template handles maximum number of parameters:  template<typename RT, typename P1 = void,                        typename P2 = void,                        typename P3 = void>  class FunctionPtrT {    public:      enum { NumParams = 3 };      typedef RT (*Type)(P1,P2,P3);  };  // partial specialization for two parameters:  template<typename RT, typename P1,                        typename P2>  class FunctionPtrT<RT, P1, P2, void> {    public:      enum { NumParams = 2 };      typedef RT (*Type)(P1,P2);  };  // partial specialization for one parameter:  template<typename RT, typename P1>  class FunctionPtrT<RT, P1, void, void> {    public:      enum { NumParams = 1 };      typedef RT (*Type)(P1);  };  // partial specialization for no parameters:  template<typename RT>  class FunctionPtrT<RT, void, void, void> {    public:      enum { NumParams = 0 };      typedef RT (*Type)();  }; 

Notice how we used the same template to "count" the number of parameters.

The functor type we are developing passes its parameters to the function pointer it encapsulates. Passing a function call argument can have side effects: If the corresponding parameter has a class type (and not a reference to a class type), its copy constructor is invoked. To avoid this extra cost, it is useful to have a type function that leaves its argument type unchanged, except if it is a class type, in which case a reference to the corresponding const class type is produced. With the TypeT template developed in Chapter 15 and our IfThenElse utility template, this is achieved fairly concisely:

  // functors/forwardparam.hpp  #ifndef FORWARD_HPP  #define FORWARD_HPP  #include "ifthenelse.hpp"  #include "typet.hpp"  #include "typeop.hpp"  //  ForwardParamT<T>::Type  is   // - constant reference for class types   // - plain type for almost all other types   // - a dummy type (  Unused  ) for type  void  template<typename T>  class ForwardParamT {    public:      typedef typename IfThenElse<TypeT<T>::IsClassT,                                  typename TypeOp<T>::RefConstT,                                  typename TypeOp<T>::ArgT                                 >::ResultT              Type;  };  template<>  class ForwardParamT<void> {    private:      class Unused {};    public:      typedef Unused Type;  };  #endif  // FORWARD_HPP  

Note the similarity of this template with the RParam template developed in Section 15.3.1 on page 276. The difference is that we need to map the type void (which, as mentioned earlier, is used to denote an unused parameter type) to a type that can validly appear as a parameter type.

We are now ready to define the FunctionPtr template. Because we don't know a priori how many parameters it will take, we overload the function call operator for every number of parameters (up to three in our case):

  // functors/functionptr.hpp  #include "forwardparam.hpp"  #include "functionptrt.hpp"  template<typename RT, typename P1 = void,                        typename P2 = void,                        typename P3 = void>  class FunctionPtr {    private:      typedef typename FunctionPtrT<RT,P1,P2,P3>::Type FuncPtr;  // the encapsulated pointer:  FuncPtr fptr;    public:  // to fit in our framework:  enum { NumParams = FunctionPtrT<RT,P1,P2,P3>::NumParams };      typedef RT ReturnT;      typedef P1 Param1T;      typedef P2 Param2T;      typedef P3 Param3T;  // constructor:  FunctionPtr(FuncPtr ptr)       : fptr(ptr) {      }  // ''function calls'':  RT operator()() {          return fptr();      }      RT operator()(typename ForwardParamT<P1>::Type a1) {          return fptr(a1);      }      RT operator()(typename ForwardParamT<P1>::Type a1,                    typename ForwardParamT<P2>::Type a2) {          return fptr(a1, a2);      }      RT operator()(typename ForwardParamT<P1>::Type a1,                    typename ForwardParamT<P2>::Type a2,                    typename ForwardParamT<P2>::Type a3) {          return fptr(a1, a2, a3);      }  }; 

This class template works well, but using it directly can be cumbersome. A few (inline) function templates allow us to exploit the template argument deduction mechanism to alleviate this burden :

  // functors/funcptr.hpp  #include "functionptr.hpp"  template<typename RT> inline  FunctionPtr<RT> func_ptr (RT (*fp)())  {      return FunctionPtr<RT>(fp);  }  template<typename RT, typename P1> inline  FunctionPtr<RT,P1> func_ptr (RT (*fp)(P1))  {      return FunctionPtr<RT,P1>(fp);  }  template<typename RT, typename P1, typename P2> inline  FunctionPtr<RT,P1,P2> func_ptr (RT (*fp)(P1,P2))  {      return FunctionPtr<RT,P1,P2>(fp);  }  template<typename RT, typename P1, typename P2, typename P3> inline  FunctionPtr<RT,P1,P2,P3> func_ptr (RT (*fp)(P1,P2,P3))  {      return FunctionPtr<RT,P1,P2,P3>(fp);  } 

All there is left to do is to try the advanced template tool we just developed with the following little demonstration program:

  // functors/functordemo.cpp  #include <iostream>  #include <string>  #include <typeinfo>  #include "funcptr.hpp"  double seven()  {      return 7.0;  }  std::string more()  {      return std::string("more");  }  template <typename FunctorT>  void demo (FunctorT func)  {      std::cout << "Functor returns type "                << typeid(typename FunctorT::ReturnT).name() << '\n'                << "Functor returns value "                << func() << '\n';  }  int main()  {      demo(func_ptr(seven));      demo(func_ptr(more));  } 
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