4 February 2012

Using templates: Common behaviour for hierarchy of classes


This article presents a solution for a small problem, which is not often met in practice, at least in mine. But the solution itself can be interesting and even useful from the point of view of templates and type conversions. So here is the problem statement:

We have a bunch of classes Base, Base2, ..., BaseN. Each of them represents some logical object and has a public interface to make use of that object. Also some of the classes have a hierarchy of subclasses. Let class ‘Base’ inherits classes ‘Sub1’, ‘Sub2’, …, ‘SubN’. ‘Sub1’ inherits ‘Sub11’, ‘Sub12’, …, ‘Sub1M’; ‘Sub2’ inherits ‘Sub21’, ‘Sub22’, …’Sub2L’, and so on. Consider such a hierarchical tree of any size and shape derived from class ‘Base’ and similar hierarchies for each of the rest ‘BaseK’, (K changes from 2 to N) classes. Now we need a function which will do:
  • one thing for several concrete classes derived (not directly in general) from ‘Base’,
  • another thing for the whole hierarchy of ‘Base’ excluding the first group,
  • some other thing for all the remaining classes.


The first thing that comes to head is that the final function should be a template, as it should work for different types. Not affecting the generality of the problem let’s consider that the function only reads information from the objects, ie we can take a const reference as an argument. So for now we think about a function of this prototype:

template <typename T>
void do_something(const T& t);

The most general group is the third one, as it can contain classes of any relationship, so this should be the template function itself:

template <typename T>
void do_something(const T& t)
{
    // do something with t
}

Now a call to ‘do_something’ with an object of any type will do the same thing. To provide a different behavior for the ‘Base’ class, we must write an explicit specialization for the template:

template <>
void do_something(const Base&)
{
    // do another thing for Base type
}

 So now if we pass an object of type ‘Base’, a different code will be executed. But as soon as we pass an object of some other type from second group, ie a class not necessarily directly derived from ‘Base’, again the general template will be called. This is not what we want. So we need to provide a way to call the specialization for ‘Base’ for all its descendants.

In general any class type publicly derived from ‘Base’ is also a ‘Base’. So if we have a function which takes an object of type ‘Base’, it will also take an object of any publicly derived type. eg:

bool is_derived(const Base&)
{
       return true;
}

But we need to call this function for any other type either, so we need to overload it for any type, but we can’t make it a template as all the descendants of ‘Base’ will make their own implicit specializations at the point of instantiation. So the only reasonable way is overloading:

bool is_derived(...)
{
       return false;
}

Now we have a way to check whether the passed in object is of type ‘Base’ or derived from ‘Base’, but how can we make use of it? We still need some way to convert the derived types to the base to pass to the final template function. On top of that, as we need to call a template function, we should know the type of the object at compile time, so the ‘is_derived’ function is not of much use.

Fortunately, there is a way to perform this check at compile-time. It is the sizeof operator, which returns the size of the passed in object. But we don’t know the sizes of all classes, and, moreover, some of them may be equal, which is not convenient in this case. So we again can make use of the same mechanism as ‘is_derived’ used. But we need different return types, more precisely, types with different object sizes.

struct my_type {
       char word[2];
};

char checker(const Base&)
{
       return '0';
}

my_type checker(...)
{
       return my_type();
}

Now the ‘checker’ will return a char for all objects of type ‘Base’ or derived from ‘Base’, and my_type for all the other objects. The size of char is never equal to size of my_type, as the former is always 1, and hence the latter is at least 2. Besides that, the value of the expression sizeof(checker(<some_type>)) is known at compile-time. So we can use it as a template argument. All we have to do is provide a way to treat the objects of types derived from ‘Base’ as ‘Base’ objects. One possible solution is a template class with two arguments - one the object type, and the second the size of the object returned by ‘checker’:

template <typename T, size_t S>
class Executor;

We need two explicit specializations for argument ‘S’, exactly
  • S == 1: when checker return char, ie the object is ‘Base’ or derived from ‘Base’,
  • S == sizeof(my_type): when checker return my_type, ie the object is of other type.


template <typename T>
class Executor<T, sizeof(char)>
{
public:
       Executor(const T& obj) :
              m_obj(static_cast<const Base&>(obj))
       {
       }

       void operator()() {
              do_something(m_obj);
       }

private:
       const Base& m_obj;
};

template <typename T>
class Executor<T, sizeof(my_type)>
{
public:
       Executor(const T& obj) :
              m_obj(obj)
       {
       }

       void operator()() {
              // do something for not-related object types
       }

private:
       const T& m_obj;
};

Now we can redefine the general template ‘do_something’ the following way:

template <typename T>
void do_something(const T& t)
{
       Executor<T, sizeof(checker(t))> e(t);
       e();
}

 And the specialization for ‘Base’ is unchanged:

template <>
void do_something(const Base&)
{
    // do another thing for Base and derived from Base types
}

For the first group - several classes derived from ‘Base’ with different behaviour than ‘Base’, we have to write explicit specializations for each type.

This is just a solution for an intermediate problem, but I hope it is useful for many people who uses templates and C++ in general. Feel free to correct me if something is wrong, or ask questions if something is unclear. Thanks.

No comments:

Post a Comment