tags:

views:

61

answers:

1

I have a set of classes in a logging framework used by project A and B. I am refactoring the framework so it can be used in project B and C. The refactoring mainly consists of giving everything template parameters: project A might run on an embedded device with poor/no STL implemenatation, while B and C just run on a pc, but B is single threaded while C uses multithreading.

This works well, but results in what seems to me an awfull lot of template parameters and a rather ugly typedef mess. I need like 20 lines to typedef all classes I'm going to use, and there are also lots of classes taking a template parameter they do not use themselves, but is needed to be able to typedef another class they do use (which is not per se a bad thing, but in the end everything starts to llok really complicated). Another problem is that when I want to add some functionality to class A and it requires adding a container, class A needs an extra template paramater. As a result, all other classes seeing/using class A suddenly also need that extra parameter leading to a domino effect.

Slightly exaggerated example:

template< class string, class map, class mutex >
class MessageDestination
{
  typedef Message< string, map > message_type;
  virtual void Eat( const message_type& ) = 0;
}

template< class string, class map, class stream >
class MessageFormatter
{
  typedef Message< string, map > message_type;
  virtual void Format( const message_type&, stream& ) = 0;
}

template< class string, class map, class containerA,
          template< class, class > containerB, template< class, class > class queue, class allocator >
class ThreadedMessageAcceptor
{
  typedef Message< string, map > message_type;
  typedef MessageDestination< string, map > destination_type;
  typedef containerB< destination_type, allocator > destinations_type;
  typedef queue< message_type, allocator > messages_type;
};

I can think of some techniques to clean this up, but I'm having a hard time deciding what one or which combination to use. StackOverFlow, your help will be appreciated!

Here's the first solution I thought of, joining parameters together into the type they'll eventually form:

template< class message, class mutex >
class MessageDestination
{
  virtual void Eat( const message& ) = 0;
}

This makes it simpler, but doesn't it at the same time it kind of hides what message actually is? Suppose a user wants to provide an implementation, he does not directly see that message has to use a certain string type etc.

Another technique I thougt about, but cannot recall having seen before somwhere which makes it look suspicious, is simply defining everything in a single struct and passing that as single template parameter to everything:

struct MyTemplateParameters
{
  typedef std::string string;
  typedef std::map map;
  typedef std::queue queue;
  typedef LightMutex mutex;
  template< class A, class B >
  struct DefineContainerB
  {
    typedef containerB< A, B >::type;
  }
  //....
};

template< class parameters >
class MessageDestination
{
  typedef Message< parameters > message_type;
  virtual void Eat( const message_type& ) = 0;
};

template< class parameters >
class ThreadedMessageAcceptor
{
  typedef Message< parameters > message_type;
  typedef MessageDestination< parameters > destination_type;
  typedef parameters::DefineContainerB< destination_type, parameters::allocator >::type destinations_type;
};

This is nice as it allows specifying everything at one single point, and the typedefs to all classes will all be class XXX< MyTemplateParameters >, but again, it gives me an uneasy feeling. Is there a reason for this?

+5  A: 

The “other” technique is very common in C++. The parameters class is usually called a “trait class”.

This is the way to go (why does it give you an uneasy feeling?). It is used pervasively in the Boost libraries and other C++ libraries. Even the standard library uses it, e.g. in the std::basic_string class.


An equally well-established alternative are metafunctions. At its most basic, a metafunction is a “function” that operates on types rather than objects. So where a function takes value arguments and returns a value, a metafunctions takes template arguments and “returns” a type:

template <typename T>
struct identity {
    typedef T type;
};

The metafunction is used (“invoked”) like a normal type definition.

typedef identity<int>::type mytype; // or
identity<int>::type x;

Not very useful in this case. But consider the following common metafunction:

template <typename T>
struct remove_const {
    typedef T type;
};

template <typename T>
struct remove_const<T const> {
    typedef T type;
};

This can be used to make an arbitrary type (in particular a template argument) non-const. This is actually a type I’m currently using in a project: I have a class that accepts both const and non-const types and offers appropriate interfaces. However, internally I need to store a non-const reference. Simple, I just use the following code in my class:

typename remove_const<T>::type& _reference;

(The typename is required because T is a template argument and that makes remove_const<T>::type a dependent type. Your above example code is actually omitting quite a few required typenames – it won’t compile on several modern compilers!)

Now, how to apply this to your problem?

Create two empty marker types that specify whether your types are used on an embedded device or on a compliant compiler:

struct Embedded { };
struct Compliant { };

Now you can define your classes in terms of these, e.g.:

template<typename Spec>
class ThreadedMessageAcceptor
{
    typedef Message< Spec > message_type;
    typedef MessageDestination< Spec > destination_type;
    typedef typename Allocator< destination_type, Spec >::type allocator_type;
    typedef typename ContainerB< destination_type, allocator_type, Spec >::type destinations_type;
};

Here, Spec will be either Compliant or Embedded. So to use it on a standards compliant compiler, write:

ThreadedMessageAcceptor<Compliant> x;

The class uses the following metafunctions:

template <typename T, typename Spec>
struct Allocator { };

template <typename T, typename Alloc, typename Spec>
struct ContainerB { };

You need to remember to specialize them appropriately for your target specifications, e.g.:

template <typename T>
struct Allocator<T, Compliant> {
   typedef std::allocator<T> type;
};

template <typename T, typename Alloc>
struct ContainerB<T, Alloc, Compliant> {
    typedef std::vector<T, Alloc> type;
};

This already shows that a metafunction may have arbitrarily many arguments besides the Spec (which I’ve put last on a whim – but its placement should be consistent).

To be sure, this is more code than when using a single trait class but it has lower cohesion, logically separates concerns and is easier to reuse.

Konrad Rudolph
For more info http://www.cantrip.org/traits.html
Kedar
Modern trends go towards avoiding blobs of traits and rather providing traits as separate metafunctions, each one with a single `type` or `value` result, so I would not say "this is the way to go". Still "this is A way to go" seems good enough.
David Rodríguez - dribeas
@David: I’m all for metafunctions – in fact, I nearly posted this as the answer instead – but considering the sheer bulk of types in the code above, I still think that traits are a better solution in this case. And I wouldn’t say that traits are *generally* deprecated in favour of metafunctions.
Konrad Rudolph
@Konrad: traits do definitely ring a bell, no idea why I didn't make the link (maybe I thought they were more used in ways like traits< some_type >, not just like the MyTemplateParameters I wrote without any template parameter)The reason for the uneasy feeling is that for example MessageDestination has no business with DefineContainerB. But I just realized this could be solved by having multiple traits sets where the more complicated ones inherit a simpler one.Care to elaborate on how metafunctions could be used here?
stijn
@stijn: see updated answer.
Konrad Rudolph
Konrad, _great_ answer.
stijn