COMMS
Template library intended to help with implementation of communication protocols.
Advanced Guide to Message Dispatching

The Message Handling section of the How to Use Defined Custom Protocol page describes a basic way to dispatch message object (held by a pointer to the main interface class) to its handling function. The described approach requires support for Polymorphic Dispatch Message for Handling. However, since version v1.1 the COMMS library supports other multiple ways to dispatch a message even if its interface doesn't define polymorphic dispatch() member function.

Dispatch of the Message Object

The handler for the message object is expected to look exactly the same as described in Message Handling, i.e. to define handle() member function for every actual message type it intends to handle, handle() member function for the interface type for the ones it doesn't, and define RetType type to specify return type of the handling functions in case it's not void.

class MyHandler
{
public:
// Return type of all the handle() functions
typedef bool RetType;
// All messages to handle properly
bool handle(my_protocol::Message1<MyMessage>& msg) {...}
bool handle(my_protocol::Message2<MyMessage>& msg) {...}
bool handle(my_protocol::Message3<MyMessage>& msg) {...}
...
// All other (don't care) messages
bool handle(my_protocol::MyMessage& msg) {...}
};

There are several different implemented ways to dispatch a message object, held by a pointer to its interface class, to its appropriate handling function.

Every way has its advantages and disadvantages, please read on and choose one that suites your needs. There are some definition commonly used for all the examples below.

All the mentioned below dispatch functions are defined in comms/dispatch.h header.

#include "comms/dispatch.h"
Contains extra logic to help with dispatching message types and objects.

The used name for the common interface class (see Defining Message Interface Class) is going to be MyMessage

using MyMessage = comms::Message<...>;
Main interface class for all the messages.
Definition: Message.h:80

The message types that need to be supported are bundled in std::tuple and named AllMessages

using AllMessages =
std::tuple<
Message1<MyMessage>,
Message2<MyMessage>,
Message3<MyMessage>,
...
>;

Also let's assume that numeric ID of Message1 is 1, of Message2 is 2, of Message90 is 90, and so on...

Polymorphic

The polymorphic dispatch of the message object can look like this

// Numeric ID of the message object
auto id = ...
// Message object itself held by a pointer to MyMessage interface class
MsgPtr msg = ...
// Handler object
MyHandler handler;
comms::dispatchMsgPolymorphic<AllMessages>(id, *msg, handler);

At first, the comms::dispatchMsgPolymorphic() function will check (at compile time) whether the message object can be dispatched to the handler directly, i.e. the Polymorphic Dispatch Message for Handling is supported in the interface and the used handler is of a suitable type. If this is the case, the dispatch will be performed using the following call

return msg.dispath(handler);

In case the direct dispatch is not possible, the comms::dispatchMsgPolymorphic() function will analyze the provided tuple of message types (AllMessages) at compile time and generate appropriate global static dispatch tables (initialized before call to main()).

In case the numeric ID are sequential and unique with no more than 10% of the gaps (the ID of the last message is not greater than number of message types in the provided tuple multiplied by 1.1), the generated dispatch tables and logic provide O(1) runtime complexity to dispatch message object into appropriate handler.

The generated table is just an array of pointers to a dispatch method class equivalent to the code below

struct DispatchMethod
{
virtual RetType dispatch(MyMessage& msg, MyHandler& handler) const = 0;
};
// Dispatch registry
std::array<const DispatchMethod*, MaxId + 1> DispatchRegistry;

Every pointer to the array is to a global instantiation of the class below for every type in the provided tuple.

<template <typename TMessage>
struct DispatchMethodImpl : public DispatchMethod
{
virtual RetType dispatch(MyMessage& msg, MyHandler& handler) const override
{
return handler.handle(static_cast<TMessage&>(msg));
}
};

The code inside the comms::dispatchMsgPolymorphic() function will use the message ID as an index to access the registry array and invoke the virtual dispatch() method. In case the accessed cell is empty, the downcasting to the right message type won't occur and handle() message for the interface of the handler object will be invoked.

In case the IDs of the message types in the provided tuple are too sparse, The registry array will be packed (no holes inside) and binary search using std::lower_bound algorithm is going to be performed. In this case the DispatchMethod class will also report an ID of the message it is responsible to handle via virtual function.

struct DispatchMethod
{
virtual MsgIdParamType getId() const = 0;
virtual RetType dispatch(MyMessage& msg, MyHandler& handler) const = 0;
};
<template <typename TMessage>
struct DispatchMethodImpl : public DispatchMethod
{
virtual MsgIdParamType getId() const override
{
return TMessage::doGetId();
}
virtual RetType dispatch(MyMessage& msg, MyHandler& handler) const override
{
return handler.handle(static_cast<TMessage&>(msg);
}
};

NOTE, that the performed binary search will invoke O(log(n)) times the virtual getId() member function to find the appropriate dispatch method and then invoke virtual dispatch() one to downcast the message type and invoke appropriate handle() member function of the handler.

There can also be a case when some message has multiple forms, that implemented as different message classes, but which share the same ID. For example

using AllMessages =
std::tuple<
Message1<MyMessage>, // Has ID 1
Message2<MyMessage>, // Has ID 2
Message90_1<MyMessage>, // Has ID 90
Message90_2<MyMessage>, // Has ID 90
>;

To support such case, the comms::dispatchMsgPolymorphic() function is overloaded with new index parameter (which is index (or offset) starting from the first type in the tuple with the requested ID) to allow selection to what type to downcast

comms::dispatchMsgPolymorphic<AllMessages>(90, msg, handler); // Invokes handle(Message90_1<MyMessage>&)
comms::dispatchMsgPolymorphic<AllMessages>(90, 0, msg, handler); // Invokes handle(Message90_1<MyMessage>&)
comms::dispatchMsgPolymorphic<AllMessages>(90, 1, msg, handler); // Invokes handle(Message90_2<MyMessage>&)
comms::dispatchMsgPolymorphic<AllMessages>(90, 2, msg, handler); // Out of range - invokes handle(MyMessage&)

There is also an overload to comms::dispatchMsgPolymorphic(), which doesn't receive any numeric message ID

comms::dispatchMsgPolymorphic<AllMessages>(msg, handler);

Such call checks (at compile time) whether the message interface provides polymorphic dispatch (see Polymorphic Dispatch Message for Handling). If this is the case, then it is used to dispatch the message to the handler. If not, then the message interface definition (MyMessage) must provide Polymorphic Retrieval of Message ID to be able to retrieve ID of the message object.

SUMMARY: The runtime complexity of polymorphic dispatch can be O(1) in case the numeric IDs of the supported message types in the provided tuple are NOT too sparse (no more than 10% holes). If this is not the case the runtime complexity is O(log(n)) with multiple virtual function calls to retrieve the ID of the dispatching method. Also the downside of the polymorphic dispatch is an amount of various v-tables the compiler will have to generate, which can significantly increase the code size. It can be a problem for various embedded systems with limited ROM.

Static Binary Search

The static binary search dispatch of the message object can look like this

// Numeric ID of the message object
auto id = ...
// Message object itself held by a pointer to MyMessage interface class
MsgPtr msg = ...
// Handler object
MyHandler handler;
comms::dispatchMsgStaticBinSearch<AllMessages>(id, *msg, handler);

The comms::dispatchMsgStaticBinSearch() function generates the code equivalent to having the following folded if statements where N is number of message types

if (id < id_of_elem(N/2)) {
if (id < id_of_elem(N/4)) {
...
else if (id > id_of_elem(N/4)) {
...
}
else {
return handler.handle(static_cast<...>(msg)); // cast to appropriate type
}
} else if (id > id_of_elem(N/2)) {
if (id < id_of_elem(3N/4)) {
...
else if (id > id_of_elem(3N/4)) {
...
}
else {
return handler.handle(static_cast<...>(msg)); // cast to appropriate type
}
}
else {
return handler.handle(static_cast<...>(msg)); // cast to appropriate type
}

The runtime complexity of such code is always O(log(n)) and there are no extra v-tables and virtual functions involved.

In case there are distinct message types with the same numeric ID (multiple forms of the same message), the overloaded function with extra index parameter is provided (similar to described earlier comms::dispatchMsgPolymorphic()).

comms::dispatchMsgStaticBinSearch<AllMessages>(90, msg, handler); // Invokes handle(Message90_1<MyMessage>&)
comms::dispatchMsgStaticBinSearch<AllMessages>(90, 0, msg, handler); // Invokes handle(Message90_1<MyMessage>&)
comms::dispatchMsgStaticBinSearch<AllMessages>(90, 1, msg, handler); // Invokes handle(Message90_2<MyMessage>&)
comms::dispatchMsgStaticBinSearch<AllMessages>(90, 2, msg, handler); // Out of range - invokes handle(MyMessage&)

There is also an overload to comms::dispatchMsgStaticBinSearch(), which doesn't receive any numeric message ID

comms::dispatchMsgStaticBinSearch<AllMessages>(msg, handler);

Such call requires the message interface definition (MyMessage) to provide Polymorphic Retrieval of Message ID to be able to retrieve ID of the message object.

SUMMARY: The runtime complexity of the static binary search dispatch is always O(log(n)) regardless of how sparse or compact are IDs of the message types in the provided tuple. There are also no v-tables generated by the compiler.

Linear Switch

The linear switch dispatch of the message object can look like this

// Numeric ID of the message object
auto id = ...
// Message object itself held by a pointer to MyMessage interface class
MsgPtr msg = ...
// Handler object
MyHandler handler;
comms::dispatchMsgLinearSwitch<AllMessages>(id, *msg, handler);

The comms::dispatchMsgLinearSwitch() function generates the code equivalent to having the following folded switch statements.

switch (id) {
case id_of_elem(0):
return handler.handle(static_cast<...>(msg)); // cast to appropriate type
default:
switch(id) {
case id_of_elem(1):
return handler.handle(static_cast<...>(msg)); // cast to appropriate type
default:
...
};
};

The runtime complexity of depends on the compiler being used. It has been noticed that clang starting from v3.9 generates efficient dispatch table with O(1) complexity when binary code is optimized for speed (-O2 ). Other main compilers, such as gcc and MSVC generate sequential comparison statements with O(n) runtime complexity.

In case there are distinct message types with the same numeric ID (multiple forms of the same message), the overloaded function with extra index parameter is provided (similar to described earlier comms::dispatchMsgPolymorphic(), and comms::dispatchMsgStaticBinSearch()).

comms::dispatchMsgLinearSwitch<AllMessages>(90, msg, handler); // Invokes handle(Message90_1<MyMessage>&)
comms::dispatchMsgLinearSwitch<AllMessages>(90, 0, msg, handler); // Invokes handle(Message90_1<MyMessage>&)
comms::dispatchMsgLinearSwitch<AllMessages>(90, 1, msg, handler); // Invokes handle(Message90_2<MyMessage>&)
comms::dispatchMsgLinearSwitch<AllMessages>(90, 2, msg, handler); // Out of range - invokes handle(MyMessage&)

There is also an overload to comms::dispatchMsgLinearSwitch(), which doesn't receive any numeric message ID

comms::dispatchMsgLinearSwitch<AllMessages>(msg, handler);

Such call requires the message interface definition (MyMessage) to provide Polymorphic Retrieval of Message ID to be able to retrieve ID of the message object.

SUMMARY: The usage of linear switch dispatch is there for real "stuntmen". If you are using clang compiler, able and willing to analyze generated binary code, and require optimal performance, then consider using linear switch dispatch. For all other cases its usage is not recommended.

Default Way to Dispatch

The COMMS library also provides a default way to dispatch message object without specifying type of the dispatch and allowing the library to choose the best one (see comms::dispatchMsg()).

// Numeric ID of the message object
auto id = ...
// Message object itself held by a pointer to MyMessage interface class
MsgPtr msg = ...
// Handler object
MyHandler handler;
comms::dispatchMsg<AllMessages>(id, *msg, handler);

In such case the COMMS library will check whether the direct invocation over dispatch() member function exposed by the message interface class is possible (see Polymorphic Dispatch Message for Handling) or the condition of O(1) polymorphic dispatch tables holds true (no more than 10% holes in the used IDs) and use polymorphic dispatch in this case. Otherwise static binary search one will be used.

In case there are distinct message types with the same numeric ID (multiple forms of the same message), the overloaded function with extra index parameter is provided similar to other dispatch methods described above.

using AllMessages1 =
std::tuple<
Message1<MyMessage>,
Message2<MyMessage>,
Message3<MyMessage>
>;
comms::dispatchMsg<AllMessages1>(1, msg, handler); // Equivalent to using comms::dispatchMsgPolymorphic()
using AllMessages2 =
std::tuple<
Message1<MyMessage>,
Message2<MyMessage>,
Message90_1<MyMessage>
>;
comms::dispatchMsg<AllMessages2>(1, msg, handler); // Equivalent to using comms::dispatchMsgStaticBinSearch()

To verify what dispatch policy is being used the COMMS library provides compile time inquiry functions comms::dispatchMsgIsPolymorphic() and comms::dispatchMsgIsStaticBinSearch() for that purpose.

static_assert(comms::dispatchMsgIsPolymorphic<AllMessages1, MyMessage, MyHandler>(), "Unexpected dispatch type");
static_assert(!comms::dispatchMsgIsStaticBinSearch<AllMessages1, MyMessage, MyHandler>(), "Unexpected dispatch type");
static_assert(!comms::dispatchMsgIsPolymorphic<AllMessages2, MyMessage, MyHandler>(), "Unexpected dispatch type");
static_assert(comms::dispatchMsgIsStaticBinSearch<AllMessages2, MyMessage, MyHandler>(), "Unexpected dispatch type");

In case MyMessage interface class defines Polymorphic Dispatch Message for Handling functionality, the "polymorphic" dispatch method will always be chosen.

static_assert(comms::dispatchMsgIsPolymorphic<AllMessages1, MyMessage, MyHandler>(), "Unexpected dispatch type");

There is a dispatchMsgIsDirect() compile time check that can be used to verify that the direct dispatch actually being used:

static_assert(comms::dispatchMsgIsDirect<MyMessage, MyHandler>(), "Unexpected dispatch type");

Dispatch of the Message Type

In some occasions there is a need to know the exact message type given the numeric ID without having any message object present for dispatching. The classic example would be the creation of message object itself given the ID (that's what comms::MsgFactory class does). To support such cases the COMMS library provides the same 3 types of dispatching the given ID to its appropriate type.

For type dispatching the handler object is expected to look a bit different.

class MyHandler
{
public:
template <typename TMessage>
void handle() {...}
};

NOTE, that the actual type is passed to the handle() member function as a template parameter. If some types require special handling function, please use template specialization, like in the example below.

template <typename TMessage>
struct MyHandlerHelper
{
// Generic handling function
static void handle() {...}
};
template <>
struct MyHandlerHelper<my_protocol::Message1<MyMessage> >
{
// Special handling function for my_protocol::Message1<MyMessage>
static void handle() {...}
};
template <>
struct MyHandlerHelper<my_protocol::Message2<MyMessage> >
{
// Special handling function for my_protocol::Message2<MyMessage>
static void handle() {...}
};
class MyHandler
{
public:
template <typename TMessage>
void handle()
{
return MyHandlerHelper<TMessage>::handle();
}
};

Similar to Dispatch of the Message Object all the mentioned below dispatch functions are defined in comms/dispatch.h header.

#include "comms/dispatch.h"

The message types that need to be supported are bundled in std::tuple and named AllMessages

using AllMessages =
std::tuple<
Message1<MyMessage>,
Message2<MyMessage>,
Message3<MyMessage>,
...
>;

All the type dispatchMsgType*() methods described below return bool which in case of being true indicates that the type was successfully found and appropriate handle() member function of the handler object being called. The return of false indicates that the appropriate type hasn't been provided in AllMessages tuple.

Polymorphic

Just like with Polymorphic dispatch of the message object, polymorphic dispatch of the message type generates similar dispatch tables with virtual functions for O(1) or O(log(n)) runtime complexity depending on how sparse the IDs in the provided tuple are.

MyHandler handler;
bool typeFound = dispatchMsgTypePolymorphic<AllMessages>(id, handler);

Please see comms::dispatchMsgTypePolymorphic() for reference.

Please note, that in case there are distinct message types with the same numeric ID (multiple forms of the same message), the overloaded function with extra index parameter is also provided.

using AllMessages =
std::tuple<
Message1<MyMessage>, // Has ID 1
Message2<MyMessage>, // Has ID 2
Message90_1<MyMessage>, // Has ID 90
Message90_2<MyMessage>, // Has ID 90
>;
dispatchMsgTypePolymorphic<AllMessages>(1, handler); // returns true
dispatchMsgTypePolymorphic<AllMessages>(1, 0, handler); // returns true, same as above
dispatchMsgTypePolymorphic<AllMessages>(1, 1, handler); // returns false
dispatchMsgTypePolymorphic<AllMessages>(90, handler); // returns true, handles Message90_1<MyMessage>
dispatchMsgTypePolymorphic<AllMessages>(90, 0, handler); // returns true, same as above
dispatchMsgTypePolymorphic<AllMessages>(90, 1, handler); // returns true, handles Message90_2<MyMessage>
dispatchMsgTypePolymorphic<AllMessages>(90, 2, handler); // returns false

Static Binary Search

Similar to Static Binary Search dispatch of the message object, static binary search dispatch of the message type generates code equivalent to mentioned folded if statements with O(log(n)) runtime complexity.

MyHandler handler;
bool typeFound = dispatchMsgTypeStaticBinSearch<AllMessages>(id, handler);

Please see comms::dispatchMsgTypeStaticBinSearch() for reference.

Please note, that in case there are distinct message types with the same numeric ID (multiple forms of the same message), the overloaded function with extra index parameter is also provided.

using AllMessages =
std::tuple<
Message1<MyMessage>, // Has ID 1
Message2<MyMessage>, // Has ID 2
Message90_1<MyMessage>, // Has ID 90
Message90_2<MyMessage>, // Has ID 90
>;
dispatchMsgTypeStaticBinSearch<AllMessages>(1, handler); // returns true
dispatchMsgTypeStaticBinSearch<AllMessages>(1, 0, handler); // returns true, same as above
dispatchMsgTypeStaticBinSearch<AllMessages>(1, 1, handler); // returns false
dispatchMsgTypeStaticBinSearch<AllMessages>(90, handler); // returns true, handles Message90_1<MyMessage>
dispatchMsgTypeStaticBinSearch<AllMessages>(90, 0, handler); // returns true, same as above
dispatchMsgTypeStaticBinSearch<AllMessages>(90, 1, handler); // returns true, handles Message90_2<MyMessage>
dispatchMsgTypeStaticBinSearch<AllMessages>(90, 2, handler); // returns false

Linear Switch

Similar to Linear Switch dispatch of the message object, linear switch dispatch of the message type generates code equivalent to mentioned folded switch statements with O(1) runtime complexity when compiled with clang compiler v3.9 and above, and O(n) runtime complexity for other major compilers.

MyHandler handler;
bool typeFound = dispatchMsgTypeLinearSwitch<AllMessages>(id, handler);

Please see comms::dispatchMsgTypeLinearSwitch() for reference.

Please note, that in case there are distinct message types with the same numeric ID (multiple forms of the same message), the overloaded function with extra index parameter is also provided.

using AllMessages =
std::tuple<
Message1<MyMessage>, // Has ID 1
Message2<MyMessage>, // Has ID 2
Message90_1<MyMessage>, // Has ID 90
Message90_2<MyMessage>, // Has ID 90
>;
dispatchMsgTypeLinearSwitch<AllMessages>(1, handler); // returns true
dispatchMsgTypeLinearSwitch<AllMessages>(1, 0, handler); // returns true, same as above
dispatchMsgTypeLinearSwitch<AllMessages>(1, 1, handler); // returns false
dispatchMsgTypeLinearSwitch<AllMessages>(90, handler); // returns true, handles Message90_1<MyMessage>
dispatchMsgTypeLinearSwitch<AllMessages>(90, 0, handler); // returns true, same as above
dispatchMsgTypeLinearSwitch<AllMessages>(90, 1, handler); // returns true, handles Message90_2<MyMessage>
dispatchMsgTypeLinearSwitch<AllMessages>(90, 2, handler); // returns false

Default Way to Dispatch

The COMMS library also provides a default way to dispatch message type without specifying type of the dispatch and allowing the library to choose the best one (see comms::dispatchMsgType()).

// Numeric ID of the message object
auto id = ...
// Handler object
MyHandler handler;
comms::dispatchMsgType<AllMessages>(id, handler);

In such case the COMMS library will check whether the condition of O(1) polymorphic dispatch tables holds true (no more than 10% holes in the used IDs) and use polymorphic dispatch in this case. Otherwise static binary search one will be used.

In case there are distinct message types with the same numeric ID (multiple forms of the same message), the overloaded function with extra index parameter is provided similar to other dispatch methods described above.

using AllMessages1 =
std::tuple<
Message1<MyMessage>,
Message2<MyMessage>,
Message3<MyMessage>
>;
comms::dispatchMsgType<AllMessages1>(1, handler); // Equivalent to using comms::dispatchMsgTypePolymorphic()
using AllMessages2 =
std::tuple<
Message1<MyMessage>,
Message2<MyMessage>,
Message90_1<MyMessage>
>;
comms::dispatchMsgType<AllMessages2>(1, msg, handler); // Equivalent to using comms::dispatchMsgTypeStaticBinSearch()

Just like with message object dispatching the same compile time comms::dispatchMsgTypeIsPolymorphic() and comms::dispatchMsgTypeIsStaticBinSearch() inquiry functions can be used to verify the dispatch policy for types being used.

static_assert(comms::dispatchMsgTypeIsPolymorphic<AllMessages1>(), "Unexpected dispatch type");
static_assert(!comms::dispatchMsgTypeIsStaticBinSearch<AllMessages1>(), "Unexpected dispatch type");
static_assert(!comms::dispatchMsgTypeIsPolymorphic<AllMessages2>(), "Unexpected dispatch type");
static_assert(comms::dispatchMsgTypeIsStaticBinSearch<AllMessages2>(), "Unexpected dispatch type");