Dispatching and Handling
When a new message arrives, its appropriate object is created, and the contents are deserialised using read()
member function, described in previous chapter. It is time to dispatch it to an appropriate handling function. Many developers use the switch
statement or even a sequence of dynamic_cast
s to identify the real type of the message object and call appropriate handling function.
As you may have guessed, this is pretty inefficient, especially when there are more than 7-10 messages to handle. There is a much better way of doing a dispatch operation by using a C++ ability to differentiate between functions with the same name but with different parameter types. It is called Double Dispatch Idiom.
Let's assume we have a handling class Handler
that is capable of handling all possible messages:
Then the definition of the messages may look like this:
Then the following code will invoke appropriate handling function in the Handler
object:
Please note, that the Message
interface class doesn't require the definition of the Handler
class, the forward declaration of the latter is enough. The Handler
also doesn't require the definitions of all the actual messages being available, forward declarations of all the message classes will suffice. Only the implementation part of the Handler
class will require knowledge about the interface of the messages being handled. However, the public interface of the Handler
class must be known when compiling dispatchImpl()
member function of any ActualMessageX
class.
Eliminating Boilerplate Code
You may also notice that the body of all dispatchImpl()
member functions in all the ActualMessageX
classes is going to be the same:
The problem is that *this
expression in every function evaluates to the object of different type.
The apperent code duplication may be eliminated using Curiously Recurring Template Pattern idiom.
Please note, that ActualMessageX
provide their own type as a template parameter to their base class MessageBase
and do not require to implement dispatchImpl()
any more. The class hierarchy looks like this:
Handling Limited Number of Messages
What if there is a need to handle only limited number of messages, all the rest just need to be ignored. Let's assume the protocol defines 10 messages: ActualMessage1
, ActualMessage2
, ..., ActualMessage10
. The messages that need to be handled are just ActualMessage2
and ActualMessage5
, all the rest ignored. Then the definition of the Handler
class will look like this:
In this case, when compiling dispatchImpl()
member function of ActualMessage2
and ActualMessage5
, the compiler will generate invocation code for appropriate handle()
function. For the rest of the message classes, the best matching option will be invocation of handle(Message&)
.
Polymorphic Handling
There may be a need to have multiple handlers for the same set of messages. It can easily be achieved by making the Handler
an abstract interface class and defining its handle()
member functions as virtual.
No other changes to dispatch functionality is required:
Generic Handler
Now it's time to think about the required future effort of extending the handling functionality when new messages are added to the protocol and their respective classes are implemented. It is especially relevant when Polymorphic Handling is involved. There is a need to introduce new virtual handle(...)
member function for every new message that is being added.
There is a way to delegate this job to the compiler using template specialisation. Let's assume, that all the message types, which need to be handled, are bundled into a simple declarative statement of std::tuple
definition:
Then the definition of the generic handling class will be as following:
The code above generates virtual handle(TCommon&)
function for the common interface class, which does nothing by default. It also creates a separate virtual handle(...)
function for every message type provided in TAll
tuple. Every such function upcasts the message type to its interface class TCommon
and invokes the handle(TCommon&)
.
As the result simple declaration of
is equivalent to having the following class defined:
From now on, when new message class is defined, just add it to the AllMessages
tuple definition. If there is a need to override the default behaviour for specific message, override the appropriate message in the handling class:
REMARK: Remember that the Handler
class was forward declared when defining the Message
interface class? Usually it looks like this:
Note, that Handler
is declared to be a class
, which prevents it from being a simple typedef
of GenericHandler
. Usage of typedef
will cause compilation to fail.
CAUTION: The implementation of the GenericHandler
presented above creates a chain of N + 1 inheritances for N messages defined in AllMessages
tuple. Every new class adds a single virtual function. Many compilers will create a separate vtable
for every such class. The size of every new vtable
is greater by one entry than a previous one. Depending on total number of messages in that tuple, the code size may grow quite big due to growing number of vtable
s generated by the compiler. It may be not suitable for some systems, especially bare-metal. It is possible to significantly reduce number of inheritances using more template specialisation classes. Below is an example of adding up to 3 virtual functions in a single class at once. You may easily extend the example to say 10 functions or more.
Last updated