The Curious Case of the Compile-Time Function

The Curious Case of the Compile-Time Function

By Phil Bass

Overload, 12(62):, August 2004


(An Exercise in Template Meta-Programming)

A Crime Has Been Committed

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:

Exhibit 1: The event/callback library in action.

// A class of objects that monitor some event.
class Observer {
public:
  Observer(Event<int>& event)
    : callback(bind_1st(memfun(
                  &Observer::handler), this))
    , connection(event, &callback) {}
private:
  void handler(int);  // the event handler
  typedef Callback::Adapter<
               void (Observer::*)(int)>::type
      Callback_Type;
  Callback_Type callback; // a function object
  Callback::Connection<int> connection;
                         // event <-> callback
};

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 [ 1 ] 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:

Exhibit 2: The goal.

// A class of objects that monitor some event.
class Observer {
public:
  Observer(Event<int>& event)
     : callback(event, &Observer::handler,
                this) {}
private:
  void handler(int);  // the event handler
  Bound_Callback<void (Observer::*)(int)>
     callback;
};

The question is how should we write the Bound_Callback<Pmf> template?

Suspects and Red Herrings

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!

A Promising Lead

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:

Exhibit 3: A simple meta-function.

template<typename Arg>
struct meta_function {
  typedef <some type expression involving Arg>
       type;
};

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.

Exhibit 4: A magical meta-function.

template<typename Pmf>
                 // Result (Class::*Pmf)(Arg)
struct argument {
  typedef <magic involving Pmf> type;
                 // type == Arg
};

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.

Exhibit 5: Using a meta-function.

// A callback bound to an event.
template<typename Pmf>
class Bound_Callback
  : public Callback::Function<typename argument<Pmf>::type> {
public:
  typedef typename argument<Pmf>::type Arg;
  typedef typename result<Pmf>::type Result;
  typedef typename class_<Pmf>::type Class;
  Bound_Callback(Event<Arg>& event, Pmf f,
                 Class* p)
    : pointer(p), function(f)
    , connection(event, this) {}
  Result operator()(Arg value) {
    return (pointer->*function)(value);
  }
private:
  Class* pointer;
  Pmf function;
  Callback::Connection<Arg> connection;
};

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?"

Reviewing the Evidence

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:

Exhibit 6: A meta-function for clairvoyants.

template<typename Result, typename Class,
         typename Arg>
struct argument {
  typedef Arg type;
};

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 .

Exhibit 7: Extracting the parameter type.

// Declaration of general template
template<typename Pmf> struct argument;
// Partial specialisation for pointers to
// member functions
template<typename Result, typename Class,
         typename Arg>
struct argument<Result (Class::*)(Arg)> {
  typedef Arg type;
};

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:

Exhibit 8: Using the result<Pmf> meta-function.

typedef result<
        void (Observer::*)(int)>::type Result;
Result* null_pointer = 0;  // Result is void

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 .

The Case is Closed

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.

References

[Bass] Phil Bass, "Implementing the Observer Pattern in C++", Overload 53 , February 2003.

[boost] See www.boost.org



[ 1 ] These are not-quite-standard variations of std::bind1st() and std::mem_fun() developed in-house for reasons that are not important here.






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.