15.2 Type Functions

Ru-Brd

The initial traits example demonstrates that you can define behavior that depends on types. This is different from what you usually implement in programs. In C and C++, functions more exactly can be called value functions : They take some values as parameters and return another value as a result. Now, what we have with templates are type functions : a function that takes some type arguments and produces a type or constant as a result.

A very useful built-in type function is sizeof , which returns a constant describing the size (in bytes) of the given type argument. Class templates can also serve as type functions. The parameters of the type function are the template parameters, and the result is extracted as a member type or member constant. For example, the sizeof operator could be given the following interface:

  // traits/sizeof.cpp  #include <stddef.h>  #include <iostream>  template <typename T>  class TypeSize {    public:      static size_t const value = sizeof(T);  };  int main()  {      std::cout << "TypeSize<int>::value = "                << TypeSize<int>::value << std::endl;  } 

In what follows we develop a few more general-purpose type functions that can be used as traits classes in this way.

15.2.1 Determining Element Types

For another example, assume that we have a number of container templates such as vector<T> , list<T> , and stack<T> . We want a type function that, given such a container type, produces the element type. This can be achieved using partial specialization:

  // traits/elementtype.cpp  #include <vector>  #include <list>  #include <stack>  #include <iostream>  #include <typeinfo>  template <typename T>  class ElementT;  // primary template  template <typename T>  class ElementT<std::vector<T> > {  // partial specialization  public:      typedef T Type;  };  template <typename T>  class ElementT<std::list<T> > {  // partial specialization  public:      typedef T Type;  };  template <typename T>  class ElementT<std::stack<T> > {  // partial specialization  public:      typedef T Type;  };  template <typename T>  void print_element_type (T const & c)  {      std::cout << "Container of "                << typeid(typename ElementT<T>::Type).name()                << " elements.\n";  }  int main()  {      std::stack<bool> s;      print_element_type(s);  } 

The use of partial specialization allows us to implement this without requiring the container types to know about the type function. In many cases, however, the type function is designed along with the applicable types and the implementation can be simplified. For example, if the container types define a member type value_type (as the standard containers do), we can write the following:

 template <typename C>  class ElementT {    public:      typedef typename C::value_type Type;  }; 

This can be the default implementation, and it does not exclude specializations for container types that do not have an appropriate member type value_type defined. Nonetheless, it is usually advisable to provide type definitions for template type parameters so that they can be accessed more easily in generic code. The following sketches the idea:

 template <typename T1, typename T2,  ...  >  class X {    public:      typedef T1   ;      typedef T2   ;   }; 

How is a type function useful? It allows us to parameterize a template in terms of a container type, without also requiring parameters for the element type and other characteristics. For example, instead of

 template <typename T, typename C>  T sum_of_elements (C const& c); 

which requires syntax like sum_of_elements<int>(list) to specify the element type explicitly, we can declare

 template<typename C>  typename ElementT<C>::Type sum_of_elements (C const& c); 

where the element type is determined from the type function.

Note that the traits can be implemented as an extension to the existing types. Thus, you can define these type functions even for fundamental types and types of closed libraries.

In this case, the type ElementT is called a traits class because it is used to access a trait of the given container type C (in general, more than one trait can be collected in such a class). Thus, traits classes are not limited to describing characteristics of container parameters but of any kind of "main parameters."

15.2.2 Determining Class Types

With the following type function we can determine whether a type is a class type:

  // traits/isclasst.hpp  template<typename T>  class IsClassT {    private:      typedef char One;      typedef struct { char a[2]; } Two;      template<typename C> static One test(int C::*);      template<typename C> static Two test();    public:      enum { Yes = sizeof(IsClassT<T>::test<T>(0)) == 1 };      enum { No = !Yes };  }; 

This template uses the SFINAE (substitution-failure-is-not-an-error) principle of Section 8.3.1 on page 106. The key to exploit SFINAE is to find a type construct that is invalid for function types but not for other types, or vice versa. For class types we can rely on the observation that the pointer-to-member type construct int C::* is valid only if C is a class type.

The following program uses this type function to test whether certain types and objects are class types:

  // traits/isclasst.cpp  #include <iostream>  #include "isclasst.hpp"  class MyClass {  };  struct MyStruct {  };  union MyUnion {  };  void myfunc()  {  }  enumE{e1}e;  // check by passing type as template argument  template <typename T>  void check()  {      if (IsClassT<T>::Yes) {          std::cout << " IsClassT " << std::endl;      }      else {          std::cout << " !IsClassT " << std::endl;      }  }  // check by passing type as function call argument  template <typename T>  void checkT (T)  {      check<T>();  }  int main()  {      std::cout << "int: ";      check<int>();      std::cout << "MyClass: ";      check<MyClass>();      std::cout << "MyStruct:";      MyStruct s;      checkT(s);      std::cout << "MyUnion: ";      check<MyUnion>();      std::cout << "enum:    ";      checkT(e);      std::cout << "myfunc():";      checkT(myfunc);  } 

The program has the following output:

 int:      !IsClassT  MyClass:  IsClassT  MyStruct: IsClassT  MyUnion:  IsClassT  enum:     !IsClassT  myfunc(): !IsClassT 

15.2.3 References and Qualifiers

Consider the following function template definition:

  // traits/apply1.hpp  template <typename T>  void apply (T& arg, void (*func)(T))  {      func(arg);  } 

Consider also the following code that attempts to use it:

  // traits/apply1.cpp  #include <iostream>  #include "apply1.hpp"  void incr (int& a)  {      ++a;  }  void print (int a)  {      std::cout << a << std::endl;  }  int main()  {      intx=7;      apply (x, print);      apply (x, incr);  } 

The call

 apply (x, print) 

is fine. With T substituted by int , the parameter types of apply() are int& and void(*)(int) , which corresponds to the types of the arguments. The call

 apply (x, incr) 

is less straightforward. Matching the second parameter requires T to be substituted with int& , and this implies that the first parameter type is int& & , which ordinarily is not a legal C++ type. Indeed, the original C++ standard ruled this an invalid substitution, but because of examples like this, a later technical corrigendum (a set of small corrections of the standard; see [Standard02]) made T& with T substituted by int& equivalent to int& . [7]

[7] Note that we still cannot write int& & . This is similar to the fact that T const allows T to be substituted with int const , but an explicit int const const is not valid.

For C++ compilers that do not implement the newer reference substitution rule, we can create a type function that applies the "reference operator" if and only if the given type is not already a reference. We can also provide the opposite operation: Strip the reference operator (if and only if the type is indeed a reference). And while we are at it, we can also add or strip const qualifiers. [8] All this is achieved using partial specialization of the following generic definition:

[8] The handling of volatile and const volatile qualifiers is omitted for brevity, but they can be handled similarly.

  // traits/typeop1.hpp  template <typename T>  class TypeOp {  // primary template  public:      typedef T         ArgT;      typedef T         BareT;      typedef T const   ConstT;      typedef T &       RefT;      typedef T &       RefBareT;      typedef T const & RefConstT;  }; 

First, a partial specialization to catch const types:

  // traits/typeop2.hpp  template <typename T>  class TypeOp <T const> {  // partial specialization for  const  types  public:      typedef T const   ArgT;      typedef T         BareT;      typedef T const   ConstT;      typedef T const & RefT;      typedef T &       RefBareT;      typedef T const & RefConstT;  }; 

The partial specialization to catch reference types also catches reference-to- const types. Hence, it applies the TypeOp device recursively to obtain the bare type when necessary. In contrast, C++ allows us to apply the const qualifier to a template parameter that is substituted with a type that is already const . Hence, we need not worry about stripping the const qualifier when we are going to reapply it anyway:

  // traits/typeop3.hpp  template <typename T>  class TypeOp <T&> {  // partial specialization for references  public:      typedef T &                        ArgT;      typedef typename TypeOp<T>::BareT  BareT;      typedef T const                    ConstT;      typedef T &                         RefT;      typedef typename TypeOp<T>::BareT & RefBareT;      typedef T const &                   RefConstT;  }; 

References to void types are not allowed. It is sometimes useful to treat such types as plain void however. The following specialization takes care of this:

  // traits/typeop4.hpp  template<>  class TypeOp <void> {  // full specialization for  void    public:      typedef void       ArgT;      typedef void       BareT;      typedef void const ConstT;      typedef void       RefT;      typedef void       RefBareT;      typedef void       RefConstT;  }; 

With this in place, we can rewrite the apply template as follows:

 template <typename T>  void apply (typename TypeOp<T>::RefT arg, void (*func)(T))  {      func(arg);  } 

and our example program will work as intended.

Remember that T can no longer be deduced from the first argument because it now appears in a name qualifier. So T is deduced from the second argument only, and T is used to create the type of the first parameter.

15.2.4 Promotion Traits

So far we have studied and developed type functions of a single type: Given one type, other related types or constants were defined. In general, however, we can develop type functions that depend on multiple arguments. One example that is very useful when writing operator templates are so-called promotion traits . To motivate the idea, let's write a function template that allows us to add two Array containers:

 template<typename T>  Array<T> operator+ (Array<T> const&, Array<T> const&); 

This would be nice, but because the language allows us to add a char value to an int value, we really would prefer to allow such mixed-type operations with arrays too. We are then faced with determining what the return type of the resulting template should be:

 template<typename T1, typename T2>  Array<  ???  > operator+ (Array<T1> const&, Array<T2> const&); 

A promotion traits template allows us to fill in the question marks in the previous declaration as follows:

 template<typename T1, typename T2>  Array<typename Promotion<T1, T2>::ResultT>  operator+ (Array<T1> const&, Array<T2> const&); 

or, alternatively, as follows:

 template<typename T1, typename T2>  typename Promotion<Array<T1>, Array<T2> >::ResultT  operator+ (Array<T1> const&, Array<T2> const&); 

The idea is to provide a large number of specializations of the template Promotion to create a type function that matches our needs. Another application of promotion traits was motivated by the introduction of the max() template, when we want to specify that the maximum of two values of different type should have the "the more powerful type" (see Section 2.3 on page 13).

There is no really reliable generic definition for this template, so it may be best to leave the primary class template undefined:

 template<typename T1, typename T2>  class Promotion; 

Another option would be to assume that if one of the types is larger than the other, we should promote to that larger type. This can by done by a special template IfThenElse that takes a Boolean nontype template parameter to select one of two type parmeters:

  // traits/ifthenelse.hpp  #ifndef IFTHENELSE_HPP  #define IFTHENELSE_HPP  // primary template: yield second or third argument depending on first argument  template<bool C, typename Ta, typename Tb>  class IfThenElse;  // partial specialization:  true  yields second argument  template<typename Ta, typename Tb>  class IfThenElse<true, Ta, Tb> {    public:      typedef Ta ResultT;  };  // partial specialization:  false  yields third argument  template<typename Ta, typename Tb>  class IfThenElse<false, Ta, Tb> {    public:      typedef Tb ResultT;  };  #endif  // IFTHENELSE_HPP  

With this in place, we can create a three-way selection between T1 , T2 , and void , depending on the sizes of the types that need promotion:

  // traits/promote1.hpp   // primary template for type promotion  template<typename T1, typename T2>  class Promotion {    public:      typedef typename              IfThenElse<(sizeof(T1)>sizeof(T2)),                         T1,                         typename IfThenElse<(sizeof(T1)<sizeof(T2)),                                             T2,                                             void                                            >::ResultT                        >::ResultT ResultT;  }; 

The size-based heuristic used in the primary template works sometimes, but it requires checking. If it selects the wrong type, an appropriate specialization must be written to override the selection. On the other hand, if the two types are identical, we can safely make it to be the promoted type. A partial specialization takes care of this:

  // traits/promote2.hpp   // partial specialization for two identical types  template<typename T>  class Promotion<T,T> {    public:      typedef T ResultT;  }; 

Many specializations are needed to record the promotion of fundamental types. A macro can reduce the amount of source code somewhat:

  // traits/promote3.hpp  #define MK_PROMOTION(T1,T2,Tr)            \      template<> class Promotion<T1, T2> {  \        public:                             \          typedef Tr ResultT;               \      };                                    \                                            \      template<> class Promotion<T2, T1> {  \        public:                             \          typedef Tr ResultT;               \      }; 

The promotions are then added as follows:

  // traits/promote4.hpp  MK_PROMOTION(bool, char, int)  MK_PROMOTION(bool, unsigned char, int)  MK_PROMOTION(bool, signed char, int)   

This approach is relatively straightforward, but requires the several dozen possible combinations to be enumerated. Various alternative techniques exist. For example, the IsFundaT and IsEnumT templates could be adapted to define the promotion type for integral and floating-point types. Promotion would then need to be specialized only for the resulting fundamental types (and user -defined types, as shown in a moment).

Once Promotion is defined for fundamental types (and enumeration types if desired), other promotion rules can often be expressed through partial specialization. For our Array example:

  // traits/promotearray.hpp  template<typename T1, typename T2>  class Promotion<Array<T1>, Array<T2> > {    public:      typedef Array<typename Promotion<T1,T2>::ResultT> ResultT;  };  template<typename T>  class Promotion<Array<T>, Array<T> > {    public:      typedef Array<typename Promotion<T,T>::ResultT> ResultT;  }; 

This last partial specialization deserves some special attention. At first it may seem that the earlier partial specialization for identical types ( Promotion<T,T> ) already takes care of this case. Unfortunately, the partial specialization Promotion<Array<T1>, Array<T2> > is neither more nor less specialized than the partial specialization Promotion<T,T> (see also Section 12.4 on page 200). [9] To avoid template selection ambiguity, the last partial specialization was added. It is more specialized than either of the previous two partial specializations.

[9] To see this, try to find a substitution of T that makes the latter become the former, or substitutions for T1 and T2 that make the former become the latter.

More specializations and partial specializations of the Promotion template can be added as more types are added for which a concept promotion makes sense.

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