Channels ▼
RSS

C/C++

Generalizing Observer


Generalizing Observer

Even though storing a member function pointer alone doesn't do the trick, note what happens when we store a generalized function:

// Example 3: A generalized Observer pattern
// structure, using function
//
class Subject {
  // ...
public:
  virtual void Attach( function<void (Subject*)> o ) { obs_.push_back(o); }
  virtual void Detach( function<void (Subject*)> o ) { /* ... */ }
  virtual void Notify() {
    for( list<function<void (Subject*)> >::iterator i = obs_.begin(); i != obs_.end(); ++i )
      (*i)( this );
  }
private:
  list<function<void (Subject*)> > obs_;
};

(Aside: You may have noticed that the set is now a list. Ignore that for now; we'll come back to that issue in a moment.)

The first, and moderately amusing, thing to note is that the Observer pattern no longer needs a predefined Observer base class. Of course, if you have one, that still works — here's the same example use as in Example 2, with only slight modifications needed:

class ClockTimer : public Subject { // as before
  // ...
  void Tick() {
    // ... timekeeping logic...
    Notify();	// every so often, tick
  }
};

class DigitalClock : /*...*/ public Observer { // still works
  // ...
public:
  DigitalClock( ClockTimer* t ) : timer_(t)
    { timer_->Attach( bind1st( mem_fun( &DigitalClock::OnUpdateOf ), this ) ); }
  ~DigitalClock()
    { timer_->Detach( bind1st( mem_fun( &DigitalClock::OnUpdateOf ), this ) ); }
  void OnUpdateOf( Subject* timer ) { /* query timer and redraw */ }
private:
  ClockTimer* timer_;
};

But the observer no longer needs to inherit from Observer. Further, the callback function could as easily be named anything, rather than just OnUpdateOf. For example:

class Clock2 /*... no inheritance needed...*/ {
  // ...
public:
  Clock2( ClockTimer* t ) : timer_(t)
    { timer_->Attach( bind1st( mem_fun( &Clock2::Ticker ), this ) ); }
  ~Clock2()
    { timer_->Detach( bind1st( mem_fun( &Clock2::Ticker ), this ) ); }
  void Ticker( Subject* timer ) { /* query timer and redraw */ }
private:
  ClockTimer* timer_;
};

Better still, the callback could be anything, even a nonmember function or a functor, and even one whose signature isn't exactly the same but is callable with parameter or return type conversions:

void TickMe( Subject* timer ) { /* query timer and do something */ }

class Tock {
public:
  void operator()( ClockTimer* timer ) // note, not exact match!
    { /* do whatever */ }
};

Tock tock;

ClockTimer ct;
ct.Attach( &TickMe );
ct.Attach( tock );

The possibilities, now, are truly endless: Any callable function or function-like entity, without limitation, can observe the subject, even if its signature isn't an exact match. Now that's a Subject that's really "observable"! I hope you'll agree that this is a compelling advantage of using function over one of the more hardwired alternatives, and a compelling structural generalization over the original OO structure for Observer described in [4], which originally worked only with a hardwired inheritance hierarchy and a single preset virtual function name and exact signature match.

An Important Limitation of function

Let's take another look at the generalized Observer pattern in Example 3, this time highlighting the rest of the changes that I didn't highlight before:

// Example 3, reprise
//
class Subject {
  // ...
public:
  virtual void Attach( function<void (Subject*)> o ) { obs_.push_back(o); }
  virtual void Detach( function<void (Subject*)> o ) { /* ... */ }
  virtual void Notify() {
    for( list<function<void (Subject*)> >::iterator i = obs_.begin(); i != obs_.end(); ++i )
      (*i)( this );
  }
private:
  list<function<void (Subject*)> > obs_;
};

In Example 2, the Subject stored a set of pointers to observing objects. Using a set, which stores unique elements, automatically made the subject ignore duplicate observers so that the same observer wouldn't be notified multiple times. (If that's not desirable, it would be easy to switch to using a multiset instead.) But function objects can't be put into a set, because a set is an ordered collection that has to be able to compare its elements; specifically, it requires the ability to decide whether one element is less than another, and function objects are not comparable at all, not even for equality which is the simplest comparison you can have.

Therefore, in Example 3:

  • We can't use a set to store our list of observers, because there is no operator< (or its equivalent) to enable less<> for function objects. We have to use an unordered collection, such as a list, instead. That's mostly a minor annoyance.
  • Worse still, we don't have the option of ignoring duplicates in Attach(), because there isn't even an operator== for function objects. We would need at least some way to compare for equality to determine whether the observer was already in the list or not.
  • Worst of all, there's no way to implement Detach at all, for the same reason that we can't ignore duplicates in Attach: there's no way to compare two function objects to see if they're equal to one another. In the code as it stands, once you observe, you always observe until the subject goes away.

This lack of comparison operations is one limitation of function, and it is significant. Even without it, function is still pretty useful; even if we only had == (and therefore also !=), it would be extremely useful.

For completeness, I'll point out that there is a partial workaround that unfortunately still falls short: Attach could return a cookie or iterator that the caller would be required to store and later pass back to Detach to specify which function to remove from the list. This workaround would enable us to write Detach, but it's not a realistic solution because of two shortcomings, one major and one minor: The major shortcoming is that it doesn't address the second problem of ignoring duplicates, and so it is at best a partial workaround for specifying the identity of a given function-like entity. The minor shortcoming is that it adds complication; users just want to add and remove functions, and alternative solutions exist (e.g., .NET delegates) that let users do that by simply naming the function, without requiring them to remember an additional magic cookie or token to refer to that function again later.

Suggested Extension #1: Equality Comparison

function could provide equality comparisons (== and !=). These are the minimum needed to allow functions like Attach to ignore duplicates and to make functions like Detach implementable. This section will describe an implementation that I believe is possible, if not perfect.

In brief, we want equality comparison to tell us whether calling two functions will cause the same target function to be invoked; in this case of a member function, this includes the object on which the function will be invoked. So the question is: will the two function objects invoke the same nonmember function? the same member function on the same object? the same functor object?

Afunction object always internally refers to one of three things: a nonmember function, a standard binder functor, or some other functor. (When it refers to a member function, it does so through a binder like std::bind1st which binds the member function to a particular object, and so the member function case falls into the "standard binder" category.) Further, the function object knows which of the three kinds of things it is storing. This lets us get close to specifying a workable equality comparison between two function objects a and b, using only equality comparisons on function pointers and on objects (all of which are supported in the C++ standard), plus one little extension I'll describe:

  • If a and b are not bound to the same type of entity (nonmember function, or functor), they are not equal.
  • Else if a and b are each bound to a nonmember function, they are equal if and only if their internally held function pointers have the same type and compare equal.
  • Else if a and b are each bound to a functor that is not a standard binder object, they are equal if and only if the objects have the same type and are the same object (i.e., pointers to the objects compare equal). Note that, because by default function will make a copy of its target, if you want two function objects f1 and f2 bound to the same functor obj to compare equal, be sure to use the ref or cref helper which binds a reference to the object instead of taking a copy; that is, write f1 = ref(obj) and f2 = ref(obj), not f1 = obj and f1 = obj.
  • Else a and b are each bound to a functor that is a standard binder (including possibly a binder that binds a member function pointer and an object on which it should be invoked), and they are equal if and only if the binder objects compare equal.

That last part requires a small but significant change to the standard (the "little extension" I referred to above): The existing standard binders also need to be extended to support operators == and !=. Fortunately, implementing equality comparison for binders is straightforward in general, because the standard directly supports pointer equality comparison for all pointer types, including all function pointer types. They would just have to be surfaced through the binders. This gets us the rest of the way, because we can specify a workable equality comparison between two binder objects using the same rules as those already described above for function objects, but adding a new case for member function pointers: If the binder objects c and d are each bound to a member function, they are equal if and only if their internally held member function pointers have the same type, and compare equal and both are bound to the same object (the objects they are bound to have the same type and pointers to the objects compare equal). (Note: Other binder libraries, such as the Boost binder library, may not be able to easily provide equality comparison for all operations.)

Some of you may be wondering: "If we're going to provide equality comparisons, then why we don't just take another step and allow <, <=, >, and >= too?" We could, but it would require an additional and more significant change to the standard, because the standard currently supports == and != for all pointer types, but only supports <, <=, >, and >= for pointers to nonmember functions and pointers to objects (via less <>) - it does not support those comparisons for pointers to member functions. An arbitrary ordering could be invented, but == and != alone are what's absolutely necessary to enable functions like Attach and Detach above; the incremental gain we get from having the other comparisons probably isn't justified, at least not by this example.


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.
 

Video