In part 1 of this article I presented an Event/Callback library intended to support the Observer pattern and hinted that it had some limitations. The library was based on the following Event class template:
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: ... }; |
Listing 1 - The Event<> class interface |
In this version of the library an Event is essentially a list of polymorphic function objects (callbacks) owned by the Event. The attach()
functions create a callback and add it to the list; the detach()
function removes a callback from the list and destroys it. The notify()
function simply calls each callback in the list passing a single parameter.
Logic Gates
The company I work for supplies industrial control systems. These systems are based on general purpose hardware components and highly configurable software. For example, we build “pods†that are part of underwater pipeline repair tools. The pods are designed to withstand conditions on the sea bed, but they contain general purpose I/O boards. As far as the software is concerned a pod is little more than a collection of digital and analogue inputs/outputs. Pods can be used to monitor and control tools for lifting and lowering sections of pipeline, cutting the pipe, welding pipes together and all sorts of ancillary operations. Different tool sets are used for different jobs and the software has to be configured accordingly. The configuration information is held in a database.
The database stores information that defines the control logic. For example, the data may indicate that a particular button on a control panel turns on a lamp or controls a pump. At present this is rather inflexible, so we considered using arbitrary networks of logic gates as a more general solution. The database would store these networks in the form of tables for each of the basic types of logic gate (NOT, AND, OR), plus a table specifying connections between the inputs and outputs of these logic gates. (Analogue inputs and outputs are not considered, here, but a similar mechanism can be envisaged for them.)
On start-up the control system software would create some logic gate objects and connect their inputs and outputs as specified in the database. The natural way to implement the connections was to use our existing Event/Callback library, which is based on the Event class template sketched in Listing1. An output is an Event and an input is a function that can be attached to such an Event.
A Worry
As always, there is a down side to this design. Because AND, OR and NOT gates are very simple a large number of them may be needed for practical control systems. And, because the software only “sees†an arbitrary network, it is not possible to control the complexity of connections by using a hierarchical data structure. The logic gates almost have to be stored as simple collections. And collections require value semantics. Unfortunately, the Event classes, and hence the logic gates that contain them, don’t have the required semantics - in particular, they can not be copied safely.
There are two well-known ways to resolve this problem. Either the Event classes are changed so that they can be safely stored in containers or some sort of value-like handles to Events are stored instead of the Events themselves.
Copyable Events
It is fairly straightforward to change the Event template so that Events can be stored in standard containers. Objects in standard containers are required to be Assignable and CopyConstructible, so implementing a suitable copy assignment operator and copy constructor will do the trick. Since an Event owns its callbacks, making a copy involves cloning the callbacks, which can be done using the “virtual constructor†technique described in [1].
So, yes, we can make Events copyable, but is it a good idea? Well, not really, because it creates another, less tractable, problem.
Consider the following scenario:
- An Event is added to a container.
- A callback is attached to the Event.
- Another Event is added to the container.
- The callback is detached from the original Event.
The program in Listing 2 illustrates this scenario.
#include <vector> #include "Event.hpp" void f(int) { return; } int main() { std::vector< Event<int> > events; events.push_back(Event<int>()); // Step 1 Event<int>::iterator i0 = events[0].attach(f); // Step 2 events.push_back(Event<int>()); // Step 3 Event<int>::iterator i1 = events[1].attach(f); events[1].detach(i1); events[0].detach(i0); // Step 4 return 0; } |
Listing 2 - Invalidating iterators |
With the implementation of std::vector that I am using, step 4 fails because step 3 invalidates the iterator created in step 2. In this case, the iterator is invalidated when the container it points into (an Event) is moved from one memory location to another. C++ doesn’t provide a mechanism for defining ‘move’ semantics, so the vector copies the original Event and destroys the original, leaving a dangling iterator.
It is the application program’s responsibility to avoid this problem. Possible fixes include: reserving space in the vector for all the events before attaching their callbacks; using a container whose push_back()
function doesn’t move existing elements; and attaching the callbacks only after all events have been added to the vector. In the tiny program shown here we can simply swap steps 2 and 3, but in general there may not be an easy fix.
There is one other avenue we might explore in our attempt to store Events in standard containers without placing a burden on the client code. Suppose we change the Event classes so that they keep track of the iterators returned by attach()
. Then, when an event is moved all iterators pointing to that event can be adjusted accordingly. But the appropriate ‘move’ semantics must be implemented using the copy constructor, assignment operator and destructor. These functions must adjust iterators pointing to the original Event when it is moved, but not when it is merely copied, assigned or destroyed. I imagine this can be achieved, but it certainly makes the Event classes less efficient and less cohesive. There has to be a better way.
Copyable Event Handles
If making Events copyable gets us into murky waters, perhaps we should be using non-copyable events and accessing them via copyable handles. The handles can be stored in standard containers and the Event iterators will remain valid when the handles are moved.
The Boost [2] shared pointer is a suitable candidate for a copyable Event handle. It is a smart pointer template with shared ownership semantics. All shared pointers pointing to the same target object share ownership of that object, so that the target object is destroyed when its last shared pointer is destroyed.
Using this technique the code in Listing 2 becomes the program in Listing 3.
#include <vector> #include <boost/shared_ptr.hpp> #include "Event.hpp" void f(int) { return; } typedef boost::shared_ptr< Event<int> > Handle; int main() { std::vector<Handle> events; events.push_back(Handle(new Event<int>())); Event<int>::iterator i0 = events[0]->attach(f); events.push_back(Handle(new Event<int>())); Event<int>::iterator i1 = events[1]->attach(f); events[1]->detach(i1); events[0]->detach(i0); return 0; } |
Listing 3 - Using copyable handles. |
Although this solves the copyability problem, it comes at a cost. Events are now allocated on the heap, the shared pointers have some house-keeping to do and there is an extra level of indirection involved in all Event accesses. Whether that cost is affordable depends on the application, but it has a “bad smell†(in the Extreme Programming sense). The purpose of an Event is to allow callbacks to be attached and there’s nothing about this that suggests Events should be stored on the heap.
Asking the Wrong Question?
In my experience, if a neat and tidy solution seems elusive it’s usually because we’re trying to solve the wrong problem. So, let’s look at the problem again and try to understand it better.
What is it about the Event classes that makes them so uncooperative? It is simply that an Event copies its callbacks when the Event itself is copied. It does so because it owns the callbacks. Does it need to own its callbacks? Well, no, it doesn’t, so let’s see what happens if we remove the responsibility for creating and destroying callbacks from the Event classes.
Firstly, the Event classes become a lot simpler - little more than a list of pointers to abstract functions. Listing 4 shows a reasonable implementation of the simpler Event. Note that the std:
: prefix has been omitted (and an unnecessary typedef
introduced) to simplify the layout of the code on the printed page.
// Abstract Function interface class. template<typename Arg> struct AbstractFunction { virtual ~AbstractFunction() {} virtual void operator() (Arg) = 0; }; // Event class template. template<typename Arg> struct Event : list<AbstractFunction<Arg>*> { void notify(Arg arg) { typedef AbstractFunction<Arg> Func; for_each( begin(), end(), bind2nd(mem_fun( &Func::operator()), arg)); } }; |
Listing 4 - Revised Event class template. |
In this version of the Event classes I have omitted the attach()
and detach()
functions because I feel the std::list member functions already do an adequate job. In fact, the full splendour of the std::list
interface has been made available to clients of the Event classes by using public inheritance.
Making Connections
Having removed the responsibility for creating and destroying callbacks from the Event classes, we can either invent something else for that purpose or pass the buck to the client code. I propose to offer the programmer a choice. The new Event/Callback library will provide a Connection class template that manages callbacks itself; alternatively, the client code can create and destroy its own callbacks. Sometimes we can have our cake and eat it!
Listing 5 shows the Connection template and the Callback template used in its implementation.
// Callback class template. template<typename Arg, typename Function> class Callback : public AbstractFunction<Arg> { public: Callback(Function fn) : function(fn) {} private: virtual void operator() (Arg arg) { function(arg); } Function function; }; // Event/Callback connection. template<typename Arg> class Connection { public: template<typename Fn> Connection(Event<Arg>& ev, Fn fn) : event(ev), callback(ev.insert(ev.end(), new Callback<Arg,Fn>(fn)) {} ~Connection() { delete (*callback); event.erase(callback); } private: Event<Arg>& event; Event<Arg>::iterator callback; }; |
Listing 5 - The Callback and Connection templates. |
The Connection classes are designed for the “Resource Acquisition Is Initialisation†technique described in [1]. A callback is created and attached in the Connection’s constructor and (predictably) the callback is detached and destroyed in the Connection’s destructor. Instead of calling attach()
and detach()
the client program creates and destroys a Connection object.
Copying Connections
The Connection classes as shown here suffer from the same disease as the old Event classes - they can not be copied safely. However, an event with two connections to the same callback would invoke the callback twice each time the event occurs and it’s difficult to see what use that might be. It is tempting, therefore, to make the Connection classes non-copyable (by deriving them from the boost::noncopyable
class, for example). On the other hand, it might very well be useful to store Connections in standard containers, and for that they must be copyable.
One way to make Connections copyable is to have them store a shared-pointer to a reference-counted callback and provide appropriate copy constructor and copy assignment functions.1 Doing so opens up the possibility of iterators being invalidated, but this is no longer a problem for the Event/Callback library. It was a problem in the old library because it required the client code to store the iterator returned by attach()
and supply it to the detach()
function. There is no such requirement for Connections.
Object Lifetimes
Typically, a callback will invoke a member function. It is important, therefore, for the callback’s target object to exist when the callback executes. Similarly, an Event must continue to exist until all its Connections have been destroyed because the Connection’s destructor will erase a pointer from the Event. I think these restrictions are best treated as requirements imposed on the client code by the library. An alternative approach is to maintain enough information within the library to handle these situations internally. The Boost.Signals library [2] and the SigC++ library2 seem to offer this type of full lifetime management.
Sample Code
Listing 6 illustrates how the new version of the Event/Callback library is used. It shows a snippet from the sample program given in Part 1 of this article, slightly modified to show an explicit detach()
call and to fit into a two-column page layout. The code in the left column uses the original version of the library in which callbacks are owned by the Event; the right column shows the new version in which the callbacks are owned by a Connection object. The difference is minimal. Where the old code had an attach()
call, the new code creates a Connection object; and where the old code had an explicit detach()
, the new code uses the implicit destruction of the Connection object. In this simple program the benefit of the new design is not very striking, but it should make the Logic Gates program a whole lot easier to write (if we ever find the time to implement it).
// Old Event/Callback example #include <iostream> #include "Event.hpp" using namespace std; ... // A callback function. void print(Button::State state) { cout << "New state = " << state << endl; } // A sample program int main() { Button button; cout << "Initial state = " << button.state << endl; Event<Button::State>::iterator i = button.stateChanged.attach( print ); button.press(); button.release(); button.stateChanged.detach(i); return 0; } |
// New Event/Callback example #include <iostream> #include "Event.hpp" using namespace std; ... // A callback function. void print(Button::State state) { cout << "New state = " << state << endl; } // A sample program int main() { Button button; cout << "Initial state = " << button.state << endl; Connection<Button::State> connection( button.stateChanged, print ); button.press(); button.release(); return 0; } |
Listing 6 - Sample program. |
Roll-Your-Own Connections
The Connection classes create callbacks on the heap. In cases where callbacks are better stored as local objects or as part of larger objects the programmer can use the Callback template directly. Listing 7 shows an example in which an Observer is its own callback.
// Callback as part of an Observer class Observer : public AbstractFunction<Button::State> { public: Observer(Event<Button::State>& e) : event(e) { callback = event.insert(event.end(), this); } ~Observer() { event.erase(callback); } private: virtual void operator() (Button::State state) { cout << "Button state = " << state << endl; } Event<Button::State>& event; Event<Button::State>::iterator callback; }; |
Listing 7 - Callback as part of an Observer |
Conclusion
The Event/Callback library described in part 1 of this article seemed to serve its purpose well. Experience has shown, however, that there are circumstances where it doesn’t live up to its promise. This is called “learning†and I believe it illustrates once again that software development is a young technology.
More experience is needed with the new Event/Callback library before we can be confident that it, too, is not flawed, but I’m fairly confident that the new is better than the old.
Phil Bass
phil@stoneymanor.demon.co.uk
References
1 Bjarne Stroustrup, The C++ Programming Language, Addison Wesley, ISBN 0-201-889554-4.
2 See http://www.boost.org/
Overload Journal #53 - Feb 2003 + Programming Topics
Browse in : |
All
> Journals
> Overload
> 53
(9)
All > Topics > Programming (877) Any of these categories - All of these categories |