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.
Unlike before, there is also an intermediate template type.
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.
The message processor interface is similar to the previous framework except this time Receive is now pure virtual.
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.
The specialisations are defined for all of the other instances.
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.
Processor2 will handle Message1 and Message3.
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.
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.