Passing data using generic message-passing facilities is common in C++ applications. Such techniques are often used for passing data between threads and to/from GUI components. But message passing can still be hard to implement well because generic message-passing facilities exhibit excessive coupling, lack of type safety, and monolithic message-handling functions.
In this article, I present a technique that uses the fill power of C++ to avoid these pitfallsmessages can be passed without causing undue coupling, type safety is maintained, and monolithic functions are avoided. The only translation units that need to know the details of a message are those containing the source and handler functions for that specific message type.
Traditional Techniques
Probably the most widespread message-passing technique is the use of a message structure with a specific member holding a type code that identifies the message. This is largely due to the use of this technique with C-based APIs, such as those of X11 and Microsoft Windows.
In this approach, the message structure either has general fields, which are reused with different meanings for each message, or it is the first member of a larger structure, the type of which is determined by the type code. The Windows API uses the former technique, and the X11 API the latter. In either case, the receiving code has to check the type-code member to decide how to deal with the message.
The problems with these techniques are the lack of type safety, monolithic handler functions, and the necessity of managing the type codes to ensure an appropriate level of uniqueness.
In particular, the lack of type safety means that the recipient has to cast the message data to an appropriate type before using it. This is something that is easy to get wrong, especially when copying and pasting the code (not an unusual scenario given the similarity between the code for receiving each message type), as the compiler will not warn about mistakes.
The lack of type safety also has an additional consequenceit is not possible to simply and reliably pass resources or variable-sized data through the message system because the sender doesn't always know when (or even whether) the message has been processed.
For its part, the monolithic handler function is a result of having to check the type code to determine what type of message has been received, and therefore how to process it. Such a handler function commonly is implemented as a large switch statement or an if-else-if chain. Frameworks such as MFC provide macros to ease this problem, but they do not entirely alleviate it.
The final issue is that of managing the type codes. It needs to be clear to the receiving code what the message is, so that it can be handled correctly. Therefore, each type code needs to be unique within some context accessible to the recipient. The Windows API, for example, specifies ranges of message types that can have different meanings to different applications, and other ranges that can have different meanings to different windows or GUI components within an application. In general, there needs to be a list of all the type codes that are required to be unique within a given context, so that their uniqueness can be checked. This list often takes the form of a header file that defines the type codes and is included anywhere that needs to know one of the type codes within it. This can easily lead to coupling between parts of the application that happen to use message type codes from the same set, but are not otherwise related. Such excessive coupling can lead to lengthy recompiles for simple changes.
Object-Oriented Techniques
A common feature of OO techniques is that all the message classes derive from a common base class. This replaces the explicit type code with the compiler's knowledge of the actual type. Not only that, but it provides an important advantage over the C-style techniquestype safety. Provided the destructor of the common base class is virtual, the derived message classes can then freely manage resources, such as variable-sized data, and they can be cleaned up in the destructor. The only requirement is that the recipient destroys the message objects correctly, whether or not they are handled.
The management of type codes is now replaced with managing classes. This is a much simpler task, since the range of possible names is unlimited, and clashes can be resolved through the use of namespaces.
Keep It Simple
The simplest OOP technique is to just replace the checking of the type code with a check on the actual type of the message using dynamic_cast. However, this still suffers from having a monolithic handlerrather than a chain of comparisons on the type-code fields, there is now a chain of comparisons involving dynamic_cast; see Listing One.
void handleMessage(Message* message) { if(Message1* m=dynamic_cast<Message1*>(message)) { handleMessage1(m); } else if(Message2* m=dynamic_cast<Message2*>(message)) { handleMessage2(m); } // ... }
In general, the dependencies are reduced because only the message sources and the recipient classes need to know about the messages. However, the monolithic handler now needs to know about the details of the message because a full definition is required for dynamic_castthe C-style techniques don't require the handler to know the details of the message if it dispatches to another function to do the actual processing.
Double Dispatch
Direct testing of a class's type using dynamic_cast is generally indicative of a design problem; this is what virtual functions are designed for. However, simply putting a virtual function on the message class doesn't achieve anything usefulit would tie the processing to the message, which defeats the purpose of sending a message in the first place.
The key to Double Dispatch is that the virtual function in the message class takes the handler as a parameter, then calls another function on the handler, passing itself as a parameter. Because this second call back to the handler is done from the actual derived message class, the exact message type is known, and an appropriate function on the handler can be called, either through overload resolution or by separately named functions (Listing Two).
class Message { public: virtual void dispatch(MessageHandler* handler)=0; }; class Message1: public Message { void dispatch(MessageHandler* handler) { handler->process(this); } }; class Message2: public Message { void dispatch(MessageHandler* handler) { handler->process(this); } }; // other message classes class MessageHandler { void process(Message1*); void process(Message2*); // overloads of process for other messages };
Relying on overload resolution to separate the distinct message types makes for the most symmetrythe implementation of the virtual function in each message class is now identical and can easily be wrapped in a macro if desired, or just copied directly from one message to another without chance of error.
Double Dispatch does have a drawbackhigh coupling. For the processing function in the handler class to be chosen through overload resolution, the implementation of the virtual function in the message class needs to see the full definition of the handler class, and consequently must be aware of the name of every other message type in the system. Not only that, if different handler classes are to be supported, the processing functions must be declared as virtual functions in a common handler base class, so every handler class must be aware of every message type in the system (Listing Three). Adding or deleting a message type can then cause large parts of the application to need recompiling.
class MessageHandler { virtual void process(Message1*)=0; virtual void process(Message2*)=0; virtual void process(Message3*)=0; virtual void process(Message4*)=0; // overloads of process for other messages }; class SpecificMessageHandler: public MessageHandler { void process(Message1*); void process(Message2*); void process(Message3*); void process(Message4*); // overloads of process for other messages }; class OtherSpecificMessageHandler: public MessageHandler { void process(Message1*); void process(Message2*); void process(Message3*); void process(Message4*); // overloads of process for other messages };
Dynamic Double Dispatch
It was against this backdrop that I developed the technique I call "Dynamic Double Dispatch." Whereas with the basic Double Dispatch technique, the message-processing function is selected using overload resolution at compile time (though the implementation is found in the correct handler class using the virtual function mechanism), Dynamic Double Dispatch checks that there is an appropriate processing function on the handler at runtime. The consequence of this is that the dependency problems of Double Dispatch promptly evaporate: Message types no longer need to be aware of each other, and handler classes only need to be aware of the messages they handle.
The key to this dynamic checking is to have a separate base class for each message typethe handler class inherits from the base classes that correspond to the messages it is designed to process. The dispatch function in each message class can then use dynamic_cast to check that the handler inherits from the correct base class, and consequently implements an appropriate processing function (Listing Four).
class MessageHandlerBase {}; class Message1HandlerBase: public virtual MessageHandlerBase { virtual void process(Message1*)=0; }; class Message1 { void dispatch(MessageHandlerBase* handler) { dynamic_cast<Message1HandlerBase&>(*handler).process(this); } }; class Message2HandlerBase: public virtual MessageHandlerBase { virtual void process(Message2*)=0; }; class Message2: public MessageBase { void dispatch(MessageHandlerBase* handler) { dynamic_cast<Message2HandlerBase&>(*handler).process(this); } }; // ... class SpecificMessageHandler: public Message1HandlerBase, public Message2HandlerBase { void process(Message1*); void process(Message2*); }; class OtherSpecificMessageHandler: public Message3HandlerBase, public Message4HandlerBase { void process(Message3*); void process(Message4*); };
Of course, having a completely separate handler base class for each message type would add excessive complication, as the dispatch function for each message type would now be specific to that message type, and the base classes would have to be written separately, despite being fundamentally the same, except for the message type they referenced. The key to removing such duplication is to make the base class a template, with the message type as a template parameterthe dispatch function then references the appropriate template specialization rather than a specific type; see Listing Five.
template<typename MessageType> class MessageHandler: public virtual MessageHandlerBase { virtual void process(MessageType*)=0; }; class Message1 { void dispatch(MessageHandlerBase* handler) { dynamic_cast<MessageHandler<Message1>&>(*handler).process(this); } }; class SpecificMessageHandler: public MessageHandler<Message1>, public MessageHandler<Message2> { void process(Message1*); void process(Message2*); };
With this simplification, the dispatch functions in the message classes are almost identical, but not quitethey must explicitly specify the message class to which they belong, in order to cast to the appropriate handler base class. Like many things in software, this can be solved by adding an extra layer of indirectionthe dispatch functions can all delegate to a single template function that uses template argument type deduction to determine the type of the message, and thus cast the handler to the appropriate type (Listing Six).
class Message { protected: template<typename MessageType> void dynamicDispatch(MessageHandlerBase* handler,MessageType* self) { dynamic_cast<MessageHandler<MessageType>&>(*handler).process(self); } }; class Message1: public MessageBase { void dispatch(MessageHandlerBase* handler) { dynamicDispatch(handler,this); } };
By abstracting away the differences between the dispatch functions in the message classes, the work has been pulled into one placethe definition of the function template; this provides a single point of modification for changing the behavior. The remaining dispatch functions in the message classes are all identical, and sufficiently simple that they can either be hidden behind a macro or just copied verbatim between message classes.
Unhandled Messages
The code for the dynamicDispatch function template shown so far assumes that the handler class does inherit from the appropriate SpecificMessageHandler specialization; if it doesn't, the dynamic_cast will throw an std::bad_cast exception. Sometimes this is sufficient, but other times, there is a more appropriate behaviorit might be better to discard messages that can't be handled by the recipient, or call a catch-all handler. The dynamicDispatch function can be adjusted, replacing the reference-based cast with a pointer-based cast, so the result can be tested against NULL, for example.
Where's the Trade-Off?
For all these benefits, there must be a trade-off, so what is it? In this case, there are two trade-offs. The first is the extra dynamic_cast, which adds to the performance hit of the two virtual calls. If performance is an issue, this might be problematic, but in many cases, the extra time spent on this is inconsequential. Use a profiler to identify where the bottlenecks really are.
The second trade-off is the need for the message processor to inherit from a separate base class for each message handled. This can be a source of errors because handling a new message type requires changes in two placesan appropriate entry to the base class list and the processing function. Missing the processing function is hard to do because that's the whole point, but missing the base class will only be spotted when the code is exercised. Because the processing function won't be called without it, this should be picked up quickly by unit testing, so in reality, this isn't so much of a problem, rather an inconvenience.
DDJ