18 months ago I described a version of my Event/Callback library in an Overload article [Bass]. This library is used extensively in my employer's control systems software. A typical use looks like this:
The key feature in this example is that a callback and an event/callback connection are both stored in the Observer as data members. Some attempt has been made to support this idiom by providing various helpers (the bind_1st() and memfun() function templates and the Callback::Adapter<Pmf> class template). However, there is still quite a lot of rather verbose boilerplate code. And that's a crime.
It has been clear for some time that we should be able to improve on this. There seems to be no fundamental reason, for example, why we can't combine the callback and its connection into a single class template (Bound_Callback, say) and use it like this:
The question is how should we write the Bound_Callback<Pmf> template?
The first thing that comes to mind is Boost [boost]. There's bound to be a Boost library that provides what we need. The trouble is I can't find one.
Boost.Bind provides a lovely family of bind() functions that generate all kinds of function objects. Unfortunately, their return types are unspecified, so we can't declare data members of those types.
Then there's Boost.Function, which was designed for a very similar job and does provide types we can use as data members. I believe we could, in fact, use the boost::function<> template as the callback part of our Bound_Callback. What I haven't told you, though, is that an Event<Arg> can only be connected to callbacks derived from Callback::Function<Arg>. Clearly, as boost::function<> isn't derived from this base class it doesn't provide everything we need. And, of course, it doesn't know how to make the event/callback connection, either.
So, what about Boost.Signals? Well, yes, we could replace the whole of our event/callback library with boost::signals, but I'm reluctant to do that for several (not very good) reasons. First of all, I don't like the names: "signal" is already used for something else in Unix operating systems, and "slot" is a truly bizarre word for a callback function. Secondly, Boost.Signals does more than we need or want. Specifically, I'm not convinced that a general-purpose event/callback library should do its own object lifetime management and, anyway, we couldn't use that feature in common cases like Exhibit 1. Finally, if we were to use Boost.Signals the crime would be reduced to a misdemeanour and there would be little or no motivation for this article!
The astute reader may have spotted a clue in the first exhibit. The typedef isn't there just to provide a reasonably short name for the callback type - it also shows a template meta-function in action.
A meta-function in C++ is a compile-time analogue of an ordinary (run-time) function. Well-behaved run-time functions perform an operation on a set of values supplied as parameters and generate a new value as their result. Meta-functions typically perform an operation on a set of types supplied as parameters and generate a new type as their result.
In its simplest form, a meta-function taking a single type parameter and returning another type as its result looks like this:
In C++, a meta-function always involves a template. The metafunction's parameters are the template's parameters and the metafunction's result is a nested type name or integral constant. The Boost Meta-Programming Library adopts the convention that a meta-function's result is called type (if it's a type) or value (if it's an integral constant) and that same convention is used here.
Now, suppose we had a meta-function that takes a pointerto- member-function type and returns the function's parameter type.
Similarly, we can imagine meta-functions that extract from a pointer-to-member-function the function's result type and the class of which the function is a member. We could now write a Bound_Callback<Pmf> template along the lines of Exhibit 5.
This would be exactly what we need to implement the sort of class illustrated in Exhibit 2. As Sherlock Holmes himself might say, "Well done, Watson. Now, how can we implement the argument<Pmf>, result<Pmf> and class_<Pmf> metafunctions?"
The argument<Pmf> meta-function shown in Exhibit 4 works perfectly, but only if your name is Harry Potter. Plodding detectives (and C++ compilers) can't be expected to perform magic. I was puzzled. Then I spotted something odd among the evidence:
Here's a meta-function that extracts the parameter type without using magic. It just needs a little clairvoyance. If you know in advance what the parameter type is you can use this metafunction to generate the type you need. The heroic sleuth in detective novels may seem to be clairvoyant at times but programmers are not that clever (not even pizza-stuffed, caffeinesoaked real programmers).
My search for the argument<Pmf> meta-function had run up a blind alley. It was late. I was tired. I was getting desperate. And then it hit me. We were looking for a meta-function with one parameter (like the magical one), but to implement it we need three parameters (like the one for clairvoyants). We need a specialisation.
The specialisation tells the compiler how to instantiate argument<Pmf> when Pmf is a pointer to a member function of any class, taking a single parameter of any type and returning a result of any type.
The same technique works for the result<Pmf> and class_<Pmf> meta-functions, too. In each case, the general template takes one parameter, but the specialisation takes three. The compiler performs a form of pattern matching to break down a single pointerto- member-function type into its three components. For example:
When it sees the result<Pmf> template being used the compiler compares the template argument (pointer-to-member-of-Observer) with the template parameter of the specialisation (any pointer-tomember- function). In this case the argument matches the parameter and the compiler deduces Result = void, Class = Observer, Arg = int. The compiler then instantiates the specialisation which defines result<void (Observer::*)(int)>::type as void.
So that's it. The crime is solved. All that's left is to prepare a case for presentation in court and let justice take its course. I've had enough for one day. "I'm off to the pub, anyone want to join me?", I called across the office.
"Well, that was the usual warm, friendly response", I thought, as I sat on my own with a pint. "No thanks", "Sorry, can't", "Too busy" they said. But something was still bothering me. Does Bound_Callback<Pmf> still work if we try to connect a handler function taking an int to an Event that publishes a short? And what if we need to connect an Event<Arg> to something other than a member function - like a non-member function or a function object?
These thoughts were still churning over in my mind when, sometime after midnight, I tumbled into bed and soon fell into a fitful sleep.
[boost] See www.boost.org
 These are not-quite-standard variations of std::bind1st() and std::mem_fun() developed in-house for reasons that are not important here.
Overload Journal #62 - Aug 2004 + Programming Topics
|Browse in :||
All > Topics > Programming (768)
Any of these categories - All of these categories