Designing an efficient and low resource messaging framework. Part 2

In the last part I showed a pure C++ scheme for creating a simple message processing framework using ‘double dispatch’ or the Visitor Pattern.

In this part I’m going to show you how we can mitigate some of the issues that were raised by using templates and template specialisation. We will be moving away from a pure C++ solution and partly using an old style C idiom.

This issue we are going to address is the requirement for the message processor interface to not only have to know about all of the messages in existence, but also the fact that the interface will have to be modified each a new one is added.

There is an old C technique for implementing a type of polymorphism. It requires that the ‘base’ type contains an id that specifies what concrete type it actually is. We are going to use this technique (C++ purists will be choking on their breakfast about now).

Like before, there is a base message type.

class IMessage
{
public:

  IMessage(int id)
    : messageId(id)
  {
  }

  int GetMessageId() const
  {
    return messageId;
  }

private:

  const int messageId;
};

Unlike before, there is also an intermediate template type.

template <int ID_>
class Message : public IMessage
{
public:

  Message()
    : IMessage(ID_)
  {
  }

  enum
  {
    ID = ID_
  };
};

Now let’s define the four message types again. You will notice that each message has a unique id.
I’ve used hard coded numbers here, but you would probably use named constants or enums in production code.

Messages
// Message 1.
class Message1 : public Message<1>
{
};

// Message 2.
class Message2 : public Message<2>
{
};

// Message 3.
class Message3 : public Message<3>
{
};

// Message 4.
class Message4 : public Message<4>
{
};

The message processor interface is similar to the previous framework except this time Receive is now pure virtual.

class IMessageProcessor
{
public:

  virtual void Receive(const IMessage& msg) = 0;

protected:

  virtual void Unhandled(const IMessage& msg) = 0;
};

There are now intermediate message processor templates. There is one master template and N specialisations. The value of N depends on the maximum number of messages that a particular processor will be expected to handle. This may seem like a bit of a downside, but the repetitive nature of the code lends itself to automatic code generation from a script.

The master message processor template is defined for the maximum number of messages that a processor may handle. C++ purists who have just recovered from their initial chocking episode may succumb to another when they see the static casts, but they are safe as the lines in question will tend to be written and tested at the creation of the framework. Once proven correct they will be so for all instances. The margin for error will be even less if the code generation is scripted.

// Handle the maximum number of messages
template <typename T1, typename T2 = void, typename T3 = void, typename T4 = void>
class MessageProcessor : public IMessageProcessor
{
public:

  void Receive(const IMessage& msg)
  {
    switch (msg.GetMessageId())
    {
      case T1::ID: Handle(static_cast<const T1&>(msg)); break;
      case T2::ID: Handle(static_cast<const T2&>(msg)); break;
      case T3::ID: Handle(static_cast<const T3&>(msg)); break;
      case T4::ID: Handle(static_cast<const T4&>(msg)); break;
      default: Unhandled(msg); break;
    }
  }

protected:

  virtual void Handle(const T1& msg) = 0;
  virtual void Handle(const T2& msg) = 0;
  virtual void Handle(const T3& msg) = 0;
  virtual void Handle(const T4& msg) = 0;
};

The specialisations are defined for all of the other instances.

// Three messages
template <typename T1, typename T2, typename T3>
class MessageProcessor<T1, T2, T3, void> : public IMessageProcessor
{
public:

  void Receive(const IMessage& msg)
  {
    switch (msg.GetMessageId())
    {
    case T1::ID: Handle(static_cast<const T1&>(msg)); break;
    case T2::ID: Handle(static_cast<const T2&>(msg)); break;
    case T3::ID: Handle(static_cast<const T3&>(msg)); break;
    default: Unhandled(msg); break;
    }
  }

protected:

  virtual void Handle(const T1& msg) = 0;
  virtual void Handle(const T2& msg) = 0;
  virtual void Handle(const T3& msg) = 0;
};

// Two messages
template <typename T1, typename T2>
class MessageProcessor<T1, T2, void, void> : public IMessageProcessor
{
public:

  void Receive(const IMessage& msg)
  {
    switch (msg.GetMessageId())
    {
    case T1::ID: Handle(static_cast<const T1&>(msg)); break;
    case T2::ID: Handle(static_cast<const T2&>(msg)); break;
    default: Unhandled(msg); break;
    }
  }

protected:

  virtual void Handle(const T1& msg) = 0;
  virtual void Handle(const T2& msg) = 0;
};

// One message
template <typename T1>
class MessageProcessor<T1, void, void, void> : public IMessageProcessor
{
public:

  void Receive(const IMessage& msg)
  {
    switch (msg.GetMessageId())
    {
    case T1::ID: Handle(static_cast<const T1&>(msg)); break;
    default: Unhandled(msg); break;
    }
  }

protected:

  virtual void Handle(const T1& msg) = 0;
};

Now all a concrete message processor has to do is to specify which messages it would like to handle. This has the advantage in that if a message is specified in the template parameter list, but a handler is not implemented, then a compiler error will result.

Processor1 is configured to handle Message1, Message2, Message3 and Message4. Note that the order of the message types is not important.

class Processor1 : public MessageProcessor<Message4, Message2, Message1, Message3>
{
public:

  void Handle(const Message1& msg)
  {
    std::cout << "Processor1 : Message1\n";
  }

  void Handle(const Message2& msg)
  {
    std::cout << "Processor1 : Message2\n";
  }

  void Handle(const Message3& msg)
  {
    std::cout << "Processor1 : Message3\n";
  }

  void Handle(const Message4& msg)
  {
    std::cout << "Processor1 : Message4\n";
  }

  void Unhandled(const IMessage& msg)
  {
    std::cout << "Processor1 : Unhandled IMessage\n";
  }
};

Processor2 will handle Message1 and Message3.

class Processor2 : public MessageProcessor<Message3, Message1>
{
public:

  void Handle(const Message1& msg)
  {
    std::cout << "Processor2 : Message1\n";
  }

  void Handle(const Message3& msg)
  {
    std::cout << "Processor2 : Message3\n";
  }

  void Unhandled(const IMessage& msg)
  {
    std::cout << "Processor2 : Unhandled IMessage\n";
  }
};

Although it may look as though we have considerably increased the amount of code we have to write there are a number important things to note.
The first is that each concrete message processor’s vtable will only be as large as it needs to be. It is not dependent on the total number of messages in the system; only on the number of messages that it must handle.
The second is that the introduction of a new message will not require any of the processor interfaces to be modified.
Another is that the templates do not have to be rewritten every time a message processor framework is required. The base classes and templates may be part of a shared source code library.
One other advantage is that the messages do not have to be able to access any members of the processor interface meaning that the handlers may now be ‘protected’.

The framework is used in exactly the same way as before.

int main()
{
  Processor1 p1;
  Processor2 p2;

  Message1 m1;
  Message2 m2;
  Message3 m3;
  Message4 m4;

  p1.Receive(m1); // "Processor1 : Message1"
  p1.Receive(m2); // "Processor1 : Message2"
  p1.Receive(m3); // "Processor1 : Message3"
  p1.Receive(m4); // "Processor1 : Message4"

  p2.Receive(m1); // "Processor2 : Message1"
  p2.Receive(m2); // "Processor2 : Unhandled IMessage"
  p2.Receive(m3); // "Processor2 : Message3"
  p2.Receive(m4); // "Processor2 : Unhandled IMessage"

  return 0;
}

The output of the example is also the same as before.

Processor1 : Message1
Processor1 : Message2
Processor1 : Message3
Processor1 : Message4
Processor2 : Message1
Processor2 : Unhandled IMessage
Processor2 : Message3
Processor2 : Unhandled IMessage

In the next post I will show how to improve efficiency a little more by eliminating most of the virtual functions.

Download the example code

John Wellbelove

John Wellbelove

Director of Aster Consulting Ltd
United Kingdom
I have been involved in technology and computer systems for all of my working life and have amassed considerable knowledge of designing and implementing systems that are both performant and correct. My role normally encompasses the entire project life-cycle, from specification to maintenance phase. Most systems I have worked on have required high speed and deterministic performance, often within a highly constrained platform. I am experienced in designing and adapting algorithms to solutions that are both space and time efficient, avoiding the normal overheads of standard solutions. Acting as a mentor for colleagues has often been a significant, though unofficial, part of my role.

I administer an open source project on Github.
See http://www.etlcpp.com/

Leave a Reply

Your email address will not be published. Required fields are marked *