Introduction


The header file, include/classTraits, exists to simplify the life of people trying to develop template classes and functions that deal with a wide variety of template parameters -- perhaps parameters that were not designed with reusability in mind. The tools defined in classTraits make working around nonconforming template parameters possible -- but the process is not necessarily easy or pretty.

These techniques are often called "template meta-programming" and the classTraits header is very similar to the functionality found the boost headerfile with the same name. include/classTraits is however commented so that you can understand how it works.


Traits Class Contents

When reading the template struct declarations in classTraits, understand that the goal of these declarations is to create "types" with nested compile time constants and nested sub-types.

Each template struct in classTraits contains one or both of:


Using Computed Types To Overcome Interface Annoyances

No one declares variables of the types defined in classTraits. These types only exist because the compiler can be used to construct nested members "value" and "type" automatically from the template parameters.

For example, member type, typename removeCV<T> :: type , is the same as type T except that it is not const and not volatile. This is helpful in templates that need to declare variables of the same type as a template parameter and initialize them to some value:


With this declaration:

   template<class T> void function(T &t)
   {
     T tmp = t + 1;   // won't compile if "T" is "const int"

     std::cout << tmp;
     
   }

This code fragment won't compile:

   ...

   void caller(int const &r)
   {
     function(r); // will cause a compile error because r is 'const'
   }


A solution to this problem is to modify function() to use the removeCV template like this:


   template<class T> void function(T &t)
   {
     typename removeCV<T>::type tmp = t + 1;   // compiles!

     // tmp is always a writable type 

     std::cout << tmp;
     
   }

There are of course other solutions -- each with its on risks and rewards.


Using Computed Values to specialy template Types and to select among Algorithm alternatives

Compile time constants include the following syntactic components:

They are interesting for several reasons, but of particular interest here is the fact that compile time constants can be used to specialize template classes which have integral parameters.

Consider this template:


  template<int selector, class FirstType, class SecondType>
  class Implementation; // no default -- you must specialize

Here, a template class, Implementation, is specified which requires three template parameters: a selector integer, and two other types. Since there is no implementation of Implementation, to get any use from the signature one has to specialize it for all interesting cases -- or more importantly partially specialize it:


  template<class A, class B>
  class Implementation<0, A, B>
  {
    // an implementation set with selector == 0
  };

  template<class C, class D>
  class Implementation<1, C, D>
  {
    // an implementation set with selector == 1
  };

  ...
  // handle all interesting selector based implementations

Luckily for us, a "true" boolean expression evaluates to 1 and false evaluations to 0. Given the above specializations of implementation, we can then declare a variable or use a template signature which is computed given a compile time constant expression -- such as an enumeration value.

The header file, include/classTraits, defines many template structs whose job is to compute the value of a nested enumeration type for use as a selector value when using Implementation-like template classes. In the following example, notice the use of isPointerType::value


  Implementation< isPointerType<int>::value, int, long> V1;

  Implementation< isPointerType<char*>::value, char*, long> V2;

Here, isPointerType is one of structs defined in classTraits that exists for the purpose of computing its "value" member -- which here is used to decide which of the implementations of class Implementation will be used to declare two different variables: V1, and V2.

Some of the template signatures computing a value member are:

As mentioned earlier, the principle use of these templates is to let you select between one implementation of a template class and another.

The approach shown above for selecting between implementations is a bit clunkier than necessary. In classTraits, the template, EvalTypeIf , lets you pick between two existing class using one of the above selectors. Consider this template function:


  template<class T>
  void doit(T &t)
  {

    typename EvalTypeIf< isClassType<T>::value, 
			 PODSolution<T>,
			 ClassSolution<T>
		       >::type 
		       variableName;

    variableName.doit(); // perform the needed function.

    
  }


Here, variableName, is defined to be either of type

-- depending on whether or not T is a plain old data type or not a class object. Obviously both classes must support the same member function -- doit() -- for this approach to work.

Solutions for plain old data types, like int, short, arrays, etc can involve memcpy in stead of copy construction and might turn out to be significantly faster -- depending on the algorithm.

Note that the EvalTypeIf template can be used with any constant expression, not just the value member of the class traits detector classes. For example:


    void nonTemplateFunction()
    {
	EvalTypeIf< sizeof(SomeTypedefName) > sizeof(double),
		    char,
		    long
		  >::type
                  variable;

	std::cout << sizeof(variable);
    }

Here, the data type of variable depends on definition of some typedef name -- whose actual value might vary from system to system, and this apporach lets you handle the machine differences more effectively.

Note also that you don't have to use the typename keyword in this scenario -- it is restrictured to (and required by) templates.

Generated on Wed Feb 29 22:51:48 2012 for CXXUtilities by  doxygen 1.6.3