Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

JVM Languages

Java-like Messaging System for C++ Classes


Language to the Rescue

Andrei Alexandrescu, in his excellent "Modern C++ Design," shows that C++ provides two important tools for code reuse that will be useful here. They are Templates and Multiple Inheritance. (I write these words capitalized to show my respect). Andrei says they are orthogonal, in that they are complementary and together they overcome the deficiencies of each other. I will try to mimic the way Andrei designs his classes (it is called policy-based class design) to develop Java-like messaging system without any of the limitations of the brute-force approach that I have presented earlier.

The policy-based class design should begin with finding all the questions that can influence the design of the class (like: "should the class be thread-safe or not?"). Those questions define the levels of freedom of the class and should be orthogonal. In other words, the answers to those question should not influence one another. These are questions that you can ask about the Java-like messaging system:

1. What should be the actual interface of the listener? If you want to use this system, the listener interface should be completely up to you. Also, the system should be able to integrate with new interfaces (that are designed later).

2. What information can be passed with each event? In other words, what defines the context of the event? It can be a single int value, but it could also be the complete, user-defined data type.

3. Should it be thread-safe? If you do not plan to use it in a multithreaded environment, the overhead of unnecessary synchronization would be overkill for your application. Remember: in C++ you do not pay for what you are not using. On the other hand, when multithreading enters the scene, the messaging system should not give up just because the author forgot to take it into account.

4. What collection mechanism should be used to keep all the pointers to listeners? Should it be std::vector?

These questions [2] are orthogonal. (In other words, they are independent.) The answers to these questions define the messaging system on different axes and are dimensions of the whole specification. Moreover, for some of these questions there are quite reasonable default answers. For example, I can assume that for most of the time you will not mix this system with multiple threads, (but the system should be open for this possibility). I can assume also that for most of the time std::vector pleases you as the back-end for the collection of pointers.

Listing Three presents the interface of the helper class that will be used as a wrapper for a back-end collection of pointers to the listener objects. (The implementation is omitted here. Please refer to the complete source code for details.) This helper class combines three policies that provide answers to the first, third, and fourth questions concerning the design of the messaging system.

Listing Three

// the interface of the wrapper to back-end,
// synchronized collection of pointers

template
<
    class Listener,
    class Mutex,
    class BagType
>
class ListenersCollection
{
    // this is a helper class that locks a given mutex
    // and unlocks when exiting the scope
    class MutexHolder
    {
        // ...
    };

public:

    void add(Listener *p);
    void remove(Listener *p);

    BagType getFrozen() const;

private:

    BagType bag_;
    Mutex mutex_;
};

The question: "What should be the actual interface of the listener?" is answered by the first template parameter. The back-end physical collection class is defined by the BagType template parameter. It can be any class that supports the following:

1. an iterator type should be defined in this class that allows the program to traverse the collection. (It should comply with at least the InputIterator concept.)

2. push_back(element); — this is supposed to add the element to the collection.

3. end(); — this should return the special past-the-end iterator value.

4. erase(iterator); — this method is supposed to remove the given element from the collection.

Normally, the standard sequence collections conform to these requirements, but if you have your own class that supports this functionality, you can use it here as well.

The threading policy is encapsulated in the Mutex class. The minimal interface of this class is:

class NoSynchronization
{
public:
  void lock() {}
  void unlock() {}
};

The NoSynchronization class looks somewhat trivial, but it is important to understand how it works in the ListenersCollection class. The collection helper uses the MutexHolder class to synchronize the access to the actual, physical collection. For example, the add method looks like this:

void add(Listener *p)
{
    MutexHolder m(mutex_);
    bag_.push_back(p);
}

Written like this, the add method of the helper ListenersCollection seems to always use synchronization, whether it is needed or not. In fact, MutexHolder in its constructor calls the lock() method on the mutex object. This method, however, is an empty inline function in the NoSynchronization class, so the smart compiler can optimize it out of existence. In other words, in optimized builds, the synchronization in the ListenersCollection imposes no overhead at all if the helper class is instantiated with the NoSynchronization class as the Mutex template parameter. If you really need synchronization, just substitute your own class (that implements the lock() and unlock() methods) for the Mutex template parameter and the synchronization code will be executed whenever MutexHolder objects are constructed and destroyed.

To mimic the non-synchronized collection of pointers to objects that implement the ActionListener interface (in other words, the collection that is used in Listing Two), you can simply write:

ListenersCollection
<
    ActionListener,
    NoSynchronization,
    std::vector<ActionListener*>
>
listeners_;

There is a need for clarification here. In the code above, the ActionListener is written twice. Not only it is redundant — it is obvious that a vector of ActionListeners should be used here — it is also error prone. The problem is that nothing in the code prevents the user from typing this:

ListenersCollection
<
    ActionListener1,
    NoSynchronization,
    std::vector<ActionListener2*>
>
listeners_;

which would not compile at best and would crash your system at worst. Using the template template parameter would solve the problem, but then you could not use physical collections that have a different template signature (the set of template parameters) than the standard STL classes. I am ready to compromise the code beauty for the sake of its practicality, sigh.

A few words about the interface of the helper ListenersCollection class:

  • the add method is used for registering the listener object,
  • the remove method unregisters them,
  • the getFrozen method returns the copy of the physical collection.

This copy will be needed to solve the problems that otherwise could emerge when the listener wants to register or unregister itself (generally: to change the collection) while it is in the middle of event notification, because this would render the iterator to the collection invalid. The easiest method is to make an immutable copy of the collection and use it to call all the interested recipients. The recipients will be able to change only the original collection, not the copy.

Having the helper collection class, I can show you the final messaging system. Listing Four presents the core interface (again, refer to the complete source code for details) of the Messaging class.

Listing Four

// the core interface of the Messaging class

template
<
    class Listener,
    typename Context = int,
    class Mutex = NoSynchronization,
    class BagType = std::vector<Listener*>
>
class Messaging
{
public:
    // registers the listener
    // listeners registered more than once will be notified
    // once for each registration
    void addListener(Listener *listener)
    {
        listeners_.add(listener);
    }

    // unregisters the listener
    // it doesn't hurt if it's not already registered
    // multiply registered listeners should also multiply
    // unregister themselves
    void removeListener(Listener *listener)
    {
        listeners_.remove(listener);
    }

protected:

    // this method should be called by the derived class
    // to trigger event dispatching
    void raiseEvent(const Context &context)
    {
        typedef typename BagType::iterator iterator;

        BagType frozen(listeners_.getFrozen());
        iterator it;
        iterator itend = frozen.end();

        for (it = frozen.begin(); it != itend; ++it)
        {
            Listener *p = static_cast<Listener*>(*it);
            dispatchEvent(p, context);
        }
    }

    // this is intended to be overriden in a derived class
    // and is responsible for the actual dispatching
    // this method is called for every registered listener
    // (once for each registration)
    // the overriding method should call the listener,
    // using (or not) the context as a clue
    virtual void dispatchEvent(Listener *listener,
                                const Context &context) = 0;

private:

    // the synchronized container for keeping pointers
    // to listener objects
    ListenersCollection<Listener, Mutex, BagType> listeners_;
};


This class has to incorporate the last remaining policy: the answer to the "What context should each event carry?" question. This is hidden in the Context template parameter. It can be any class you need. By default, it is int, not because int is a special meaningful type, but because void could not be used here. Templates have their limitations, too.

The Messaging class is supposed to be a base class for every class that needs to be a source of events. You can also multiply derive from Messaging instantiated with different parameters, thus making the derived class an event source for different types of events.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.