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 nooperator<
(or its equivalent) to enableless<>
forfunction
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 anoperator==
forfunction
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 inAttach
: there's no way to compare twofunction
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 function
s
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
andb
are not bound to the same type of entity (nonmember function, or functor), they are not equal. - Else if
a
andb
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
andb
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 defaultfunction
will make a copy of its target, if you want twofunction
objectsf1
andf2
bound to the same functorobj
to compare equal, be sure to use theref
orcref
helper which binds a reference to the object instead of taking a copy; that is, writef1 = ref(obj)
andf2 = ref(obj)
, notf1 = obj
andf1 = obj
. - Else
a
andb
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.