A Better Way to Process Messages - Embedded.com

A Better Way to Process Messages

Switch statements in middleware can grow to handle hundreds of defined messages, which are added ad hoc as the system matures. The alternative offered here provides for polymorphic message handling, while keeping the interface simple and manageable.

This article will present a creative implementation of a message switch class in C++ using a container from the Standard Template Library (STL). The std::

will be used to hold message IDs and associate them with pointers to specific member functions. This technique will be an alternative to a lengthy switch statement.

In the real world, new messages are added to systems and old messages are occasionally even removed. The result is a set of ad hoc message types that may not be ordered like the index into a simple array. Message types may even be grouped functionally and span large number ranges. This made me think about using the STL map to associate the message types with their processing function. Even though it would create some overhead, it seemed like a good idea for replacing a cumbersome switch statement.

Consider an enum with 100 defined values. Several lines of code for processing a message type for each value means the line count in the switch can reach hundreds or thousands of lines. Reading and maintaining a function with several thousand lines can be difficult, resulting in increased costs throughout the lifetime of the code. Every time a new ID is added, the processing function must be modified to handle the new value.

Message passing

It is common when discussing communication between objects to speak in terms of message passing. The idea of message passing in software is not new. For years, commercial products for message-oriented middleware and message queues have been available. In multi-tiered architectures, messages, typically of a heavy data content, are passed between the tiers. Protocol stacks also pass messages, usually called Protocol Data Units (PDU). A PDU usually has a header, body, and footer that fit an object-oriented model.

In the Unified Modeling Language (UML), sequence diagrams can be used to show the interactions between object instances. The interactions are described in terms of passing messages from one object to another. When you get down to the concrete instances and a C++ implementation, the messages passed are usually the invocation of the receiving object's methods with some data passed as arguments through a function call.

The message switch in this article will receive all messages and decide how to process or dispatch the request to other objects or processes in the system. The implementations presented in this article are overly simplified. They do not address topics such as threading, queues, caching, asynchronous completion, context, or session management. The article's focus is to show how the std::

can be used with pointers to non-static member functions to simplify message processing.

Pointers to functions

In C, pointers to functions support flexible implementation and abstraction. A typedef can be used to express the functions' signature and enforce compile-time type safety. The example in Listing 1 defines a pointer to a function that accepts an int as input and returns a boolean. In C, function calls typically follow the __stdcall convention which means the parameters are passed from right to left and the callee performs the stack cleanup.

Pointers to static member functions in C++ have the same calling convention as they do in C. However, pointers to non-static member functions in C++ are different, because they follow the __thiscall convention. Since non-static member functions are invoked on an object instance, the method needs to know its context, basically which instance data to use. Since all instance data is accessed implicitly through the this pointer, it is passed transparently in the function call. For the remainder of the article, let's assume we're talking about the pointers to non-static member functions, because this is how it differs from C.

Marshal Cline describes the pointer to non-static member functions in his book C++ FAQs.[1] He shows an example for storing the function pointers in an array outside of the class. His example goes on to show how the function call is made on an object instance. In this article, the pointers are stored inside the class and the function calls are executed from another class method rather than from outside the class.[2] The simple example in Listing 2 shows the C++ type definition and how it might be used somewhere in another non-static class member function:

In Classname::Bar(), the this pointer is de-referenced to get to the object instance. The pointer to the member function is also de-referenced, the dot operator is invoked, and an int argument is passed to the function. The function call uses the __thiscall calling convention. Obviously, Bar() could call Foo() directly; the example simply demonstrates the technique that will be used later.

The object model

The CMsgSwitch class will process or re-direct specific messages to other parts of the system for processing. Its primary public interface is the SendMsg function, which accepts a reference to a CMsg. Since the SendMsg function accepts references, the CMsg class will be a concrete base class. The intention is that specific message types would still be derived from this base class. Making the CMsg class an abstract base class adds more dependencies in the CMsgSwitch implementation. I want fewer dependencies and more polymorphic behavior. The old adage “minimize coupling, maximize cohesion” is still a good design principle. A UML class diagram for these two classes is shown in Figure 1. The interactions between object instances of these classes are shown in the UML sequence diagram in Figure 2.

The implementation

The declarations and definitions for the CMsg and various derived classes are shown in Listing 3. The MSG_TYPE is stored in the base class with an accessor rather than repeating the code in all derived messages. This also allows CMsgSwitch in the SendMsg function to avoid interactions with specific derived CMsg classes.

The public interface of the CMsgSwitch class includes the default constructor and the SendMsg function. The user of the class invokes the SendMsg function for all message types by passing a CMsg reference. The class has a private std::

that associates the message type with a pointer to a member function used to process that message. Listing 4 shows the CMsgSwitch declaration.

The CMsgSwitch constructor definition is shown in Listing 5. The map is initialized in the constructor with the pointers to the non-static member functions. Since the processing functions accept a reference to their specific message type, it's necessary to use the reinterpret_cast<> operation on the function pointer before storing it in the map. In all likelihood, the CMsgSwitch would be implemented as a singleton and the instance would be created early in the life of the system. Therefore, the amount of storage space required for the map entries or the time it takes to initialize it should not cause you to lose sleep. For each new MSG_TYPE, the constructor is modified and a new ProcessMsg function is declared and defined.

The SendMsg function definition details are in Listing 6. The SendMsg operates on CMsg instances, which will be concrete instances of derived messages. The SendMsg is abstracted from the details of the specific kind of CMsg that it may be handling. This means the SendMsg function never has to be modified for new messages. This is the cool part, no messy switch statement!

The std::

does all the work of associating the message type with the function to be called. It's just one line of application code. The class users never call the ProcessMsg functions since they are private, and they are only called indirectly by their function pointer from SendMsg. This lookup of the function pointer using the map takes the place of the switch statement. The SendMsg function, once written, is never modified because there is no switch statement; all message processing is done in the respective functions.

Another approach would have been to overload the SendMsg function for every message type. I chose not to do this in order to keep the public interface minimal. Also, if the client sent the message directly, there would be no opportunity for a core function to perform common message handling prior to calling the specific processing function. The CMsgSwitch would lose a little control and the processing functions would have to feed back some of this information.

As a side note, I generated assembly listings for a switch statement where the case values were in increasing order. The list files showed that the values were tested in increasing order as expected. However, despite my best attempts at re-ordering the values as they appear in the switch block, the compiler-in my case Visual C++ 6.0-always evaluated each case in increasing numerical order. I thought that it was possible to move more frequently encountered values earlier in the switch, but this simple experiment proved contrary, at least for the compiler I was using.

Stroustrup says, “The map requires that a less-than operation exist for its key types (17.1.4.1) and keeps its elements sorted so that iteration over a map occurs in order.”.[3] Because my example uses an enum, the values are constant integers and the less-than operator is a simple comparison. However, a user-defined type could be used for the key and the less-than operator overridden where the order favors the more frequently encountered messages. This would make iterating or finding the matching element faster.

Based on my observations of the switch, this kind of ordering is not possible unless the actual message IDs are ordered, which is a significant limitation and maintenance-intensive process. Ordering is possible with the map.

Example implementation

An example of how the classes are used is shown in Listing 7. To be fair, my implementation has some disadvantages. The code space required for the std::

is certainly larger than the switch statement. Another problem is that some compilers still have limited support for templates and STL. As an alternative, the map could be replaced with an associative array, associative because the message IDs were assumed to be ad-hoc, not numbered necessarily from 0 for array referencing. Finally, the problem domain was assumed to contain a large number of messages. If this is not the case, the switch is probably a better solution and the code size will be smaller.

The presented implementation has several major advantages for systems that process many defined messages. The SendMsg function of the Message Switch Class does not depend on a switch statement. Therefore, it does not change when new messages are added. The number of source lines of code (not the code size) in the function are fewer than if the switch were used, because the associative work of pairing the message IDs with the member functions is done by the std::

. The processing logic for each message type appears in the respective member functions, rather than in a massive switch, so the code is better organized and possibly less complicated. The trade-off is that the CMsgSwitch constructor must be modified for every new message. The entries in the std::

can be ordered for efficient searches, whereas the order in the switch cannot be easily changed.

The CMsgSwitch class presents a thin client interface. Interactions with other classes are made simpler because references to objects of type CMsg are received. This is in contrast to an implementation that has separate or overloaded public functions for sending every message type. Every time a new message is added, the client interface changes and grows. Since CMsgSwitch only receives CMsg references, the messages can be handled in a polymorphic manner by the corresponding private processing functions. These benefits make the presented solution a well organized and polymorphic implementation of a Message Switch Class as an alternative to a monolithic switch.

William K. Trudell Jr. has a BSEE degree and extensive experience writing software in various programming languages and application domains for 18 years. He is currently employed at Capital One, where he is implementing middleware and component software solutions for their call center. He can be reached at .

References

1. Cline, Marshall and Greg A. Lomow. C++ FAQs, 2nd edition. Reading, MA : Addison-Wesley, 1999.

2. Stroustrup, Bjarne. The C++ Programming Language, 3rd edition. Reading, MA: Addison-Wesley, 1998, pp. 418-420.

3. Ibid, p. 480.

Resources

Return to May 2001 ESP

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.