Implementing the Observer Pattern in C++ - Part 1

Implementing the Observer Pattern in C++ - Part 1

By Phil Bass

Overload, 10(52):, December 2002


Phil Bass

Introduction

The Observer design pattern is described in the "Gang of Four" book [ Gamma] as a method of propagating state changes from a Subject to its Observers. The key feature of the pattern is that Observers register with the Subject via an abstract interface. The existence of the registration interface decouples the Subject from its Observers. This makes the Observer pattern well suited to a layered architecture in which a lowerlevel Subject passes information up to its Observers in the layer above.

This idea is so important that I have formulated it as a design principle:

Use the Observer Pattern to pass information from a lower layer to the layer above.

With this in mind I developed some C++ library components intended to support this use of the pattern. These components have been used extensively in my work at Isotek. They served us well for nearly two years, but recently we began to find situations in which they failed to deliver the ease-of-use we expected. The library itself seemed fine, but unexpected complexities started to arise in classes using the library components.

In this article I shall describe the Isotek library, illustrate the situations in which it is awkward to use and begin to explore ways of tackling its limitations. I hope the library will be of interest as a partial solution to the problem of implementing the Observer pattern. A complete solution, however, is left as an exercise for the reader - because I don't know what it is!

Library Overview

The Observer support library defines a Subject as any object that publishes Events. A Subject notifies its Observers of state changes by generating appropriate Events. A Subject may publish all state changes through a single Event, or provide separate Events for different sorts of state change. For example, a Button might publish a single Event that signals both button-pressed and button-released state changes, or it might provide both a button-pressed Event and a buttonreleased Event.

An Observer registers with a Subject by attaching a suitable function (or function object) to an Event, and unregisters by detaching the function. The library supports functions taking one argument or none. In principle it could be extended to use functions with more arguments but, so far, we have not felt the need.

Conceptually, an Event is a container of polymorphic functions. Any function (or function object) that can be called with an argument of a particular type can be inserted into this container. So, for example, the following callback functions can all be inserted into an Event that generates an int value:

  void f(int); // natural signature
  int g(int); // different return type
  void h(long); // implicit argument
  // conversion

Here is some sample code showing the use of the Event<> template:

  #include <iostream>
  #include "Event.hpp"
  using namespace std;
  // A simple Subject.

  struct Button {
    enum State { released, pressed };

    Button() : state(released) {}

    void press() {
      stateChanged.notify(state=pressed);
    }

    void release() {
      stateChanged.notify(state=released);
    }

    State state;
    Event<State> stateChanged;
  };

  ostream& operator<<(ostream& os,
                      const Button::State& state) {
    return os << (state == Button::pressed ? "down" : "up");
  }

  // A callback function.
  void output(Button::State state) {
    cout << "New state = "
         << state
         << endl;
  }

  // A sample program
  int main() {
    Button button;
    cout << "Initial state = "
         << button.state << endl;
    button.stateChanged.attach(output);
    button.press();
    button.release();
    return 0;
  }

The Button class is a Subject. It publishes a single statechanged Event. When the Button::press() function is called the button goes into the Button::pressed state and it publishes the state change by calling Event<>::notify() . Similarly, calling Button::release() causes another change of state and this, too, is published by calling Event<>::notify .

In this simple example a global function is attached to the button's state changed event. There are no Observer objects.

The Event<> Template Declaration

A slightly simplified version of the Event<> class template in the library is shown below.

  template<typename Arg>
  class Event {
  public:

    // Iterator type definition.
    typedef ... iterator;

    // Destroy an Event.
    ~Event();

    // Attach a simple function to an
    // Event.
    template<typename Function> iterator attach(Function);

    // Attach a member function to an
    // Event.
    template<class Pointer, typename Member> iterator attach(Pointer, Member);

    // Detach a function from an Event.
    void detach(iterator);

    // Notify Observers that an Event
    // has occurred.
    void notify(Arg) const;

  private:
    ...
  };

The template takes a single type argument and the notify() function takes an argument of this type. Although not shown here, the library provides a specialisation for Event<void> in which the notify() function takes no argument.

There are two member template attach() functions. The first accepts a simple function; the second takes a pointer to an object and a pointer to a member function of that object. Both attach() function templates create a callback function object (stored on the heap) and insert a pointer to the callback into an internal list. This makes it possible to attach a non-member function, static member function, function object or member function to the Event<> . The only restriction is that the function takes an argument convertible to the Arg type of the Event.

The detach() function destroys the callback object specified by the iterator argument and removes the callback pointer from the list.

The notify() function simply iterates through the internal callback list calling each in turn, passing the parameter value (if any).

Finally, the destructor destroys any callback objects that are still attached.

The Event<> Template Definition

The Event implementation uses the External Polymorphism design pattern. The Event<> classes store a list of pointers to a function object base class and the attach() functions create callback objects of derived classes. The callbacks contain function pointers or function objects provided by the client code. The client-supplied objects need have no particular relationship to each other. In particular, there is no requirement for the client object types to be classes or to derive from a common base class. The callback classes perform the role of an Adapter (see [ Gamma ]), in effect, adding run-time polymorphism to the client's function types.

The implementation described here is slightly simpler than the one in the Isotek library, but it does illustrate all the essential features of the library implementation. Note that the " std:: " prefix has been omitted here to save space.

  // Event class template
  template<typename Arg>
  class Event {
    // Function object base class.
    struct AbstractFunction;

    // Concrete function object classes.
    template<typename Function>
    class Callback;

  public:
    // Iterator type definition.
    typedef list<AbstractFunction*>::iterator iterator;
    ...

  private:
    // List of function objects.
    list<AbstractFunction*> callback;
};

The library uses the standard list class, which ensures that iterators are not invalidated by insertions/deletions and callback functions can be removed efficiently in any order. Both considerations are important for Subjects that can make no assumptions about their Observers.

AbstractFunction is a simple abstract base class with a pure virtual function call operator. The Callback classes are concrete classes derived from AbstractFunction . They store a function (as a pointer or function object) and implement the virtual function call operator by calling the stored the function. The same basic mechanism is used in Andrei Alexandrescu's generic functions [ Alex ]. The Callback template, however, is less sophisticated.

  // The function object classes
  // Abstract Function.
  template<typename Arg>
  struct Event<Arg>::AbstractFunction {
    virtual ~AbstractFunction() {}
    virtual void operator()(Arg) = 0;
  };

  // Callback class template.
  template<typename Arg>
  template<typename Function>
  class Event<Arg>::Callback
    : public AbstractFunction {

  public:
    explicit Callback(Function fn) : function(fn) {}

    virtual void operator()(Arg arg) {
      function(arg);
    }

  private:
    Function function;
  };

The attach() functions create a callback on the heap and insert a pointer to its base class into the list of function objects. In principle, only the single-argument attach() function is required; the two-argument version is provided for convenience. In practice, the client code attaches a member function much more frequently than a simple function, so there is considerable value in the convenience function.

  // The attach() functions
  // Attach a simple function to an Event.
  template<typename Arg>
  template<typename Fn>
  Event<Arg>::iterator Event<Arg>::attach(Fn fn) {
    return callback.insert(callback.end(),
                           new Callback<Fn>(fn));
  }

  // Attach a member function to an Event.
  template<typename Arg>
  template<class P, typename M>
  Event<Arg>::iterator Event<Arg>::attach(P pointer,
                                          M member) {
    return attach(bind1st(mem_fun(member),pointer));
  }

The detach() function destroys the callback and erases its pointer from the list.

  // Detach a callback from an Event.
  template<typename Arg>
  void Event<Arg>::detach(iterator connection) {
    delete(*connection);
    callback.erase(connection);
  }

Notifying observers is simply a case of calling each callback in the list with the supplied parameter (if any). The code in the library and presented here uses std::for_each() to iterate through the list. The function object required as the third parameter of for_each() is built from the AbstractFunction 's function call operator using std::mem_fun() and std::bind2nd() .

  // Notify Observers that an Event has
  // occurred.
  template<typename Arg>
  void Event<Arg>::notify(Arg arg) const {
    typedef AbstractFunction Base;
    for_each(callback.begin(),
             callback.end(),
             bind2nd(mem_fun(&Base::operator()),arg));
  }

The final part of the Event<> template definition is its destructor. It just iterates through the callback list destroying any callbacks that remain. Again, the code uses for_each() and a simple function object is defined for use as its third parameter.

  // Delete function object.
  struct delete_object {
    template<typename Pointer>
    void operator()(Pointer pointer) {
      delete pointer;
    }
  };

  // Destroy an Event.
  template<typename Arg>
  Event<Arg>::~Event() {
    for_each(callback.begin(),
    callback.end(), delete_object());
  }

So Far So Good, But...

When I first wrote the Event<> template I was aware that copying an Event could cause problems. Each Event contains a list of pointers that implicitly own the callback they point to. Copying this list produces two pointers for each callback, one in the original list and one in the copy. Destroying one of the lists destroys all the callbacks. Destroying the second list leads to disaster when the code attempts to destroy each of the callbacks again.

At the time, it wasn't clear to me how this situation should be handled. Should the copying of Events be prohibited or should more appropriate copy semantics be defined? In the end I left the issue un-addressed on the assumption that any problems would quickly surface and the hope that specific cases would throw more light on it. It seems I was wrong on both counts!

The real problems only surfaced when we wanted to store objects containing Events in standard containers. I shall describe that scenario in part 2.

References

[Gamma] Gamma, Helm, Johnson and Vlissides, Design Patterns, Elements of Reusable Object-Oriented Software, Addison- Wesley, ISBN 0-201-63361-2.

[Alex] Andrei Alexandrescu, Modern C++ Design, Addison-Wesley, ISBN 0-201-70431-5.






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.