Java-like Messaging System for C++ Classes

Java uses a powerful idiom for registering event consumers to event producers. Here's a technique for doing the same thing in C++.


July 01, 2003
URL:http://www.drdobbs.com/jvm/java-like-messaging-system-for-c-classes/184403960

Some time ago I tried to learn Java. I have read a couple of good books on the subject — I think everyone has his or her own favorite Java book. Java provides programmers with some fine idioms. Some idioms result from the language itself, and some are part of the standard Java library. One of these idioms is the event publishing scheme taken by the Swing library to enable event-driven GUI programming.

In Swing, when you need a button on your window, you create it, add the button object to the "content pane" (the logical part of the window that manages its elements), and you are done. Almost. You are not usually interested in buttons that do nothing, so you add the "event handling" to it. Swing associates an action with every element of the window (for example, the action for the button is "press") and you can bind your own code with that action. This is done in two steps:

1. Write a class that implements an ActionListener interface. To do that, you need to implement its method, actionPerformed. Put all the code that needs to be executed in this method.

2. Create an object of that class and pass it to the button object. In other words, register it.

Listing One presents a very simple Java program that creates a window on the screen with one button. The event-handler for the "press" action of the button prints a message on the console window. The "press" event handler is not the only event handler, though. The second event handler is the handler for an event associated with the window being closed that causes the whole application to terminate. The whole program can be written in much more compact form (thanks to anonymous inner classes [1]), but I wanted to expose the idea of registering event consumers to event producers. This idiom is not restricted to Java (nor to Swing for that matter). Java uses this idiom quite heavily, and the code presented in Listing One uses only a fraction of the possibilities.

Listing One

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class MyJavaApp
{
    public static void main(String[] args)
    {
        // create a button
        JButton button = new JButton("Press me!");

        // register the event handler for pressing the button
        button.addActionListener(new MyButtonListener());

        // organize the layout of the window
        JPanel pane = new JPanel();
        pane.add(button);
        JFrame frame = new JFrame("My Application");
        frame.getContentPane().add(pane, BorderLayout.CENTER);

        // register the event handler for closing the window
        frame.addWindowListener(new MyWindowListener());

        frame.pack();
        frame.setVisible(true);
    }
}

// this class implements the event handler for a button
class MyButtonListener implements ActionListener
{
    public void actionPerformed(ActionEvent e)
    {
        System.out.println("I was pressed!!!");
    }
}

// this class implements the event handler for
// closing the window
class MyWindowListener extends WindowAdapter
{
    public void windowClosing(WindowEvent e)
    {
        System.exit(0);
    }
}

If this idiom is not restricted to any particular language, it would be nice to implement it in C++.

Brute-Force Approach

The first try to implement the concept of event listeners may look like the one presented in Listing Two.

Listing Two

// brute-force approach to the
// event producer-consumer idiom


#include <iostream>
#include <ostream>
#include <string>
#include <vector>

using namespace std;

// interface of the listener
class ActionListener
{
public:
    virtual void actionPerformed() = 0;
};

// some class that can generate events
class SomeClass
{
public:
    // registration function
    void addListener(ActionListener *p)
    {
        listeners_.push_back(p);
    }

    // some other functions
    // ...

    void foo();

private:
    // collection of pointers to listeners
    typedef vector<ActionListener*> ListenerBag;
    ListenerBag listeners_;

    // some other members
    // ...
};

// some function that can produce an event
void SomeClass::foo()
{
    // ...

    // produce an event and notify
    // all listeners that are registered
    ListenerBag::iterator it;
    ListenerBag::iterator itend = listeners_.end();

    for (it = listeners_.begin(); it != itend; ++it)
    {
         (*it)->actionPerformed();
    }
}

// some listener class
class MyListener : public ActionListener
{
public:
    MyListener(const string &name) : name_(name) {}
    void actionPerformed()
    {
        cout << name_
            << " : Action performed, sir!" << endl;
    }

private:
    string name_;
};

int main()
{
    MyListener l1("listener 1");
    MyListener l2("listener 2");

    SomeClass someobject;

    someobject.addListener(&l1);
    someobject.addListener(&l2);

    // ...

    someobject.foo();

    // ...

    return 0;
}

This approach has a couple of inherent problems that come with its design (these are only the problems I can find — there may be others):

1. It is not generic with respect to the interface. There is only one interface, ActionListener. In a non-trivial project, there can be a need for more than just one listener interface.

2. It is not generic with respect to the information being passed with each event. Here, the actionPerformed method does not accept any parameters, so it can be used only to notify of an event taking place. In practice, different events need to carry different amounts of information. For example, the information associated with an event may be the coordinates of a point on the screen where the mouse clicked, or it may be temperature data, such as the data produced by sensors in the core of a nuclear power station. There is no way of fixing the type of this event information.

3. It is not generic with respect to the interface vocabulary. In Listing One, the ActionListener interface has only one function signature. It is quite constraining for clients that are interested in different events, for example, not only in left-clicks but also in cursor movement. In the GUI, the window can produce many different events, like close clicked, minimalize, maximalize, and so on.

4. It does not scale up. This problem can be seen when one producer can be a source of an event defined by different listener interfaces. The brute-force approach imposes the need to implement as many collections in the event producer class as there are listener interfaces. Clearly, this approach does not promote code reuse.

5. It is not thread-safe. The STL collections are not thread-safe by their definition and SomeClass does nothing to change it. When two listener objects try to register themselves with the instance of the SomeClass class, the undefined behavior is waiting for them because the push_back method cannot be executed concurrently.

Is it THAT bad? Not quite. There is one big advantage of this brute-force strategy: it works. This should be the clue that it does not deserve to be thrown away at this stage. This strategy includes a couple good ideas. One good idea is the structure of event generator (event source). In Listing Two, it manages the collection of listeners (by pointers) that will be notified when the event takes place. Each listener can register himself with the event source by one of its public members (addListener). For the interface to be complete, the event source also needs a reverse function like removeListener so that listeners can notify it that they are no longer interested in its events.

This is the core of what is needed for implementing this Java-like communication idiom.

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:

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.

The addListener and removeListener methods are self-explanatory. The core of the messaging is hidden in two protected member methods:

This two-phase mechanism allows you to write a messaging system that will be generic with respect to the information carried by each event, however, it imposes some work on the programmer that designs the listener classes.

Example Application

Listing Five shows an example program that uses the messaging system [3].

Listing Five

// test application for messaging system

#include "Messaging.h"
#include <iostream>
#include <string>

using namespace std;

// some listener interface
struct MartianAlertListener
{
    virtual void martianLanded(const string &where) = 0;
};

// some other listener interface
struct NuclearPSListener
{
    virtual void itIsHot(int temp) = 0;
    virtual void itIsTooLate() = 0;
};

// a commong functionality of the normal person and
// the technician in the nuclear power station
class Person
{
public:
    Person(const string &name) : name_(name) {}
    string getName() const { return name_; }
private:
    string name_;
};

// a class representing the normal person
// normal person is interested in the MartianAlert events
class NormalPerson : public Person,
public MartianAlertListener
{
public:
    NormalPerson(const string &name)
        : Person(name) {}

    // here, the Person receives the notification
    virtual void martianLanded(const string &where)
    {
        cout << getName() << ": martian landed "
            << where << endl;
    }
};

// a context structure for MartianAlert events,
// holding the info considering the landing place
struct MartianContext
{
    string where_;
};

// the helper typedef
typedef
Messaging<MartianAlertListener, MartianContext>
MartianAlertSource;

// another class, representing a technician in
// the nuclear power station
// the technician is interested in
// the events related to his job
class Technician : public Person,
                   public NuclearPSListener
{
public:
    Technician(const string &name)
        : Person(name) {}

    void itIsHot(int temp)
    {
        cout << getName() << ": there is " << temp
            << " degrees in the reactor" << endl;
    }
    void itIsTooLate()
    {
        cout << getName() << ": BANG!" << endl;
    }
};

// a context structure for events in the power station,
// holding info considering:
// 1. what has happened
// 2. what is the temperature in the reactor
struct NuclearPSContext
{
    enum eWhat {it_is_hot, it_is_too_late} whathappened_;
    int temp_;
};

// the helper typedef
typedef
Messaging<NuclearPSListener, NuclearPSContext>
NuclearPSSource;


// the ultimate source of events
// note multiple inheritance
// (one for each listener type)
class EventSource : public MartianAlertSource,
                    public NuclearPSSource
{
public:
    // inherited from MartianAlertSource
    void dispatchEvent(MartianAlertListener *p,
                const MartianContext &context)
    {
        // just call the listener
        p->martianLanded(context.where_);
    }

    // inherited from NuclearPSSource
    void dispatchEvent(NuclearPSListener *p,
            const NuclearPSContext &context)
    {
        // we have a choice and parameters with some details
        if (context.whathappened_ ==
            NuclearPSContext::it_is_hot)
            p->itIsHot(context.temp_);
        else
            p->itIsTooLate();
    }

    // play a little with events
    void go()
    {
        // send a martian alert

        MartianContext ctx1;
        ctx1.where_ = "in the garden";

        // note: if this class inherits from
        // only one event source base,
        // the operator:: is not needed
        MartianAlertSource::raiseEvent(ctx1);

        // send a temperature report

        NuclearPSContext ctx2;
        ctx2.whathappened_ = NuclearPSContext::it_is_hot;
        ctx2.temp_ = 5000;
        NuclearPSSource::raiseEvent(ctx2);

        // send a "too late" event notification

        NuclearPSContext ctx3;
        ctx3.whathappened_ = NuclearPSContext::it_is_too_late;
        NuclearPSSource::raiseEvent(ctx3);
    }
};

int main()
{
    // these are object which will receive
    // event notifications:
    NormalPerson john("John");
    NormalPerson jenny("Jenny");
    NormalPerson mike("Mike");
    Technician tech1("technician 1");
    Technician tech2("technician 2");

    // this is a source of events
    EventSource source;

    // register objects as listeners in an event source
    // note: if an event source inherits from only one
    // event source base, the :: selectors are not needed
    source.MartianAlertSource::addListener(&john);
    source.MartianAlertSource::addListener(&jenny);
    source.MartianAlertSource::addListener(&mike);
    source.NuclearPSSource::addListener(&tech1);
    source.NuclearPSSource::addListener(&tech2);

    // play
    source.go();

    return 0;
}


Two listener interfaces are defined: MartianAlertListener (for notification concering the Martians landing) and NuclearPSListener (for events related to the nuclear power station). The class NormalPerson implements the MartianAlertListener interface. In the main() function, three objects of this class register themselves with the source of events. The class Technician implements the NuclearPSListener and two objects of this class are registered as well. The even source is an object of a class that derives from the Messaging class instantiated for both listener interfaces. You can see how multiple inheritance helps reuse the messaging infrastructure more than once in a single class. This introduces some naming problems, so calls to addListener have to be disambiguated explicitly. The EventSource class overrides the dispatchEvent in its two base classes, so it looks like a function overloading. The dispatchEvent override for a base class that manages martian alerts just calls the only one method in the MartianAlertListener interface for every registered listener. (This iteration is performed in the raiseEvent method in the Messaging class.) The dispatchEvent override for a base class that manages nuclear events, however, uses the context to decide which function in the listener's interface should be called and with what parameters.

This way the Messaging class can be reused to fit different communication needs of different listener interfaces.

Notes

[1] Yes, anonymous inner classes make this idiom even nicer but are not appropriate with non-trivial event handling code.

[2] There is yet another question I was able to ask: "What should happen when one listener object registers itself more than once?" The possibilities are 1) it will be notified many times 2) it will be notified only once 3) it is an error. I have difficulties with compiling the code with partial template specialization on my compiler, so I have given up (but the code should compile on a broader set of platforms). The code presented here assumes the first option.

[3] In Listing Five, the Messaging class is instantiated with the default values for synchronization and storage policies. This means that the collections of pointers to listeners are not synchronized and that the std::vector is used as a back-end of the collection.

Literature

Modern C++ Design by Andrei Alexandrescu (published by Addison-Wesley) — a very good book that opens a world of new design possibilities thanks to smart use of Templates and Multiple Inheritance.

Generative Programming by Krzysztof Czarnecki and Ulrich W. Eisenecker (Addison-Wesley) — a ground-breaking book describing parameterisation of software, at the level of analysis, specification and implementation.


Maciej Sobczak is a Ph.D. student at the Institute of Computer Science, Warsaw University of Technology. He is passionate about C++ (and experiments with other technologies, too) and is interested in distributed, object-oriented computing. You can visit him at http://www.msobczak.com.

Listing 3

Listing 3

// 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_;
};

Listing 4

Listing 4

// 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_;
};


Listing 5

Listing 5

// test application for messaging system

#include "Messaging.h"
#include <iostream>
#include <string>

using namespace std;

// some listener interface
struct MartianAlertListener
{
    virtual void martianLanded(const string &where) = 0;
};

// some other listener interface
struct NuclearPSListener
{
    virtual void itIsHot(int temp) = 0;
    virtual void itIsTooLate() = 0;
};

// a commong functionality of the normal person and
// the technician in the nuclear power station
class Person
{
public:
    Person(const string &name) : name_(name) {}
    string getName() const { return name_; }
private:
    string name_;
};

// a class representing the normal person
// normal person is interested in the MartianAlert events
class NormalPerson : public Person,
public MartianAlertListener
{
public:
    NormalPerson(const string &name)
        : Person(name) {}

    // here, the Person receives the notification
    virtual void martianLanded(const string &where)
    {
        cout << getName() << ": martian landed "
            << where << endl;
    }
};

// a context structure for MartianAlert events,
// holding the info considering the landing place
struct MartianContext
{
    string where_;
};

// the helper typedef
typedef
Messaging<MartianAlertListener, MartianContext>
MartianAlertSource;

// another class, representing a technician in
// the nuclear power station
// the technician is interested in
// the events related to his job
class Technician : public Person,
                   public NuclearPSListener
{
public:
    Technician(const string &name)
        : Person(name) {}

    void itIsHot(int temp)
    {
        cout << getName() << ": there is " << temp
            << " degrees in the reactor" << endl;
    }
    void itIsTooLate()
    {
        cout << getName() << ": BANG!" << endl;
    }
};

// a context structure for events in the power station,
// holding info considering:
// 1. what has happened
// 2. what is the temperature in the reactor
struct NuclearPSContext
{
    enum eWhat {it_is_hot, it_is_too_late} whathappened_;
    int temp_;
};

// the helper typedef
typedef
Messaging<NuclearPSListener, NuclearPSContext>
NuclearPSSource;


// the ultimate source of events
// note multiple inheritance
// (one for each listener type)
class EventSource : public MartianAlertSource,
                    public NuclearPSSource
{
public:
    // inherited from MartianAlertSource
    void dispatchEvent(MartianAlertListener *p,
                const MartianContext &context)
    {
        // just call the listener
        p->martianLanded(context.where_);
    }

    // inherited from NuclearPSSource
    void dispatchEvent(NuclearPSListener *p,
            const NuclearPSContext &context)
    {
        // we have a choice and parameters with some details
        if (context.whathappened_ ==
            NuclearPSContext::it_is_hot)
            p->itIsHot(context.temp_);
        else
            p->itIsTooLate();
    }

    // play a little with events
    void go()
    {
        // send a martian alert

        MartianContext ctx1;
        ctx1.where_ = "in the garden";

        // note: if this class inherits from
        // only one event source base,
        // the operator:: is not needed
        MartianAlertSource::raiseEvent(ctx1);

        // send a temperature report

        NuclearPSContext ctx2;
        ctx2.whathappened_ = NuclearPSContext::it_is_hot;
        ctx2.temp_ = 5000;
        NuclearPSSource::raiseEvent(ctx2);

        // send a "too late" event notification

        NuclearPSContext ctx3;
        ctx3.whathappened_ = NuclearPSContext::it_is_too_late;
        NuclearPSSource::raiseEvent(ctx3);
    }
};

int main()
{
    // these are object which will receive
    // event notifications:
    NormalPerson john("John");
    NormalPerson jenny("Jenny");
    NormalPerson mike("Mike");
    Technician tech1("technician 1");
    Technician tech2("technician 2");

    // this is a source of events
    EventSource source;

    // register objects as listeners in an event source
    // note: if an event source inherits from only one
    // event source base, the :: selectors are not needed
    source.MartianAlertSource::addListener(&john);
    source.MartianAlertSource::addListener(&jenny);
    source.MartianAlertSource::addListener(&mike);
    source.NuclearPSSource::addListener(&tech1);
    source.NuclearPSSource::addListener(&tech2);

    // play
    source.go();

    return 0;
}


Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.