http://www.tutok.sk/fastgl/callback.html
CALLBACKS IN C++ USING TEMPLATE FUNCTORS
Copyright 1994 Rich Hickey
INTRODUCTION
One of the many promises of Object-Oriented programming is that it
will allow for plug-and-play software design with re-usable
components. Designers will pull objects from their library 'shelves'
and hook them together to make software. In C++, this hooking
together of components can be tricky, particulary if they are
separately designed. We are still a long way from interoperable
libraries and application components. Callbacks provide a mechanism
whereby independently developed objects may be connected together.
They are vital for plug and play programming, since the likelihood of
Vendor A implementing their library in terms of Vendor B's classes,
or your home-brewed classes, is nil.
Callbacks are in wide use, however current implementations differ and
most suffer from shortcomings, not the least of which is their lack
of generality. This article describes what callbacks are, how they
are used, and the criteria for a good callback mechanism. It
summarizes current callback methods and their weaknesses. It then
describes a flexible, powerful and easy-to-use callback technique
based on template functors - objects that behave like functions.
CALLBACK FUNDAMENTALS
What Are Callbacks?
When designing application or sub-system specific components we often
know all of the classes with which the component will interact and
thus explicity code interfaces in terms of those classes. When
designing general purpose or library components however, it is often
necessary or desirable to put in hooks for calling unknown objects.
What is required is a way for one component to call another without
having been written in terms of, or with knowledge of, the other
component's type. Such a 'type-blind' call mechanism is often
referred to as a callback.
A callback might be used for simple notification, two-way
communication, or to distribute work in a process. For instance an
application developer might want to have a Button component in a GUI
library call an application-specific object when clicked upon. The
designer of a data entry component might want to offer the capability
to call application objects for input validation. Collection classes
often offer an apply() function, which 'applies' a member function of
an application object to the items they contain.
A callback, then, is a way for a component designer to offer a
generic connection point which developers can use to establish
communication with application objects. At some subsequent point, the
component 'calls back' the application object. The communication
takes the form of a function call, since this is the way objects
interact in C++.
Callbacks are useful in many contexts. If you use any commercial
class libraries you have probably seen at least one mechanism for
providing callbacks. All callback implementations must address a
fundamental problem posed by the C++ type system: How can you build a
component such that it can call a member function of another object
whose type is unknown at the time the component is designed? C++'s
type system requires that we know something of the type of any object
whose member functions we wish to call, and is often criticized by
fans of other OO languages as being too inflexible to support true
component-based design, since all the components have to 'know' about
each other. C++'s strong typing has too many advantages to abandon,
but addressing this apparent lack of flexibility may encourage the
proliferation of robust and interoperable class libraries.
C++ is in fact quite flexible, and the mechanism presented here
leverages its flexibility to provide this functionality without
language extension. In particular, templates supply a powerful tool
for solving problems such as this. If you thought templates were only
for container classes, read on!
Callback Terminology
There are three elements in any callback mechanism - the caller, the
callback function, and the callee.
The caller is usually an instance of some class, for instance a
library component (although it could be a function, like qsort()),
that provides or requires the callback; i.e. it can, or must, call
some third party code to perform its work, and uses the callback
mechanism to do so. As far as the designer of the caller is
concerned, the callback is just a way to invoke a process, referred
to here as the callback function. The caller determines the signature
of the callback function i.e. its argument(s) and return types. This
makes sense, because it is the caller that has the work to do, or the
information to convey. For instance, in the examples above, the
Button class may want a callback function with no arguments and no
return. It is a simple notification function used by the Button to
indicate it has been clicked upon. The DataEntryField component might
want to pass a String to the callback function and get a Boolean
return.
A caller may require the callback for just the duration of one
function, as with ANSI C's qsort(), or may want to hold on to the
callback in order to call back at some later time, as with the Button
class.
The callee is usually a member function of an object of some class,
but it can also be a stand-alone function or static member function,
that the application designer wishes to be called by the caller
component. Note that in the case of a non-static member function a
particular object/member-function pair is the callee. The function to
be called must be compatible with the signature of the callback
function specified by the caller.
Criteria for a Good Callback Mechanism
A callback mechanism in the object oriented model should support both
component and application design. Component designers should have a
standard, off-the-shelf way of providing callback services, requiring
no invention on their part. Flexibility in specifying the number and
types of argument and return values should be provided. Since the
component may be designed for use in as-yet-unthought-of
applications, the component designer should neither need to know, nor
dictate, the types of the objects which may be 'called back' by the
component.
Application developers, given a component with this standard callback
mechanism and some instance of a class with a member function
compatible with the callback function signature, should have to do no
custom 'glue' coding in order to connect the two together. Nor should
they have to modify the callee class or hand-derive a new class. If
they want to have the callback invoke a stand-alone, non-member
function, that should be supported as well.
To support this behavior the callback mechanism should be:
Object Oriented - Our applications are built with objects. In a C++
application most functionality is contained in member functions,
which cannot be invoked via normal ptr-to-functions. Non-static
member functions operate upon objects, which have state. Calling such
functions is more than just invoking a process, it is operating upon
a particular object, thus an object-oriented callback must contain
information about which object to call.
Type Safe - Type safety is a fundamental feature and benefit of C++
and any robust C++ callback mechanism must be type safe. That means
we must ensure that objects are used in compliance with their
specified interfaces, and that type rules are enforced for arguments,
return values, and conversions. The best way to ensure this is to
have the compiler do the work at compile time.
Non-Coupling - This is the fundamental goal of callbacks - to allow
components designed in ignorance of each other to be connected
together. If the mechanism somehow introduces a dependancy between
caller and callee it has failed in its basic mission.
Non-Type-Intrusive - Some mechanisms for doing callbacks require a
modification to, or derivation of, the caller or callee types. The
fact that an object is connected to another object in a particular
application often has nothing to do with its type. As we'll see
below, mechanisms that are type intrusive can reduce the flexibility
and increase the complexity of application code.
Generic - The primary differences between different callback
situations are the types involved. This suggests that the callback
mechanism should be parameterized using templates. Templates insure
consistent interfaces and names in all callback situations, and
provide a way to have any necessary support code be generated by the
compiler, not the user.
Flexible - Experience has shown that callback systems that require an
exact match between callback function and callee function signatures
are too rigid for real-world use. For instance you may encounter a
callback that passes a Derived * that you want to connect to a callee
function that takes a Base *.
CURRENT MECHANISMS
Function Model
The simplest callback mechanism is a pointer-to-function, a la ANSI
C's qsort(). Getting a stand-alone function to act upon a particular
object, however, usually involves kludges like using static or global
pointers to indicate the target object, or having the callback
function take an extra parameter (usually a pointer to the object to
act upon). The static/global pointer method breaks down when the
callback relationship exists across calls, i.e. 'I want to connect
this Button to this X and this other Button to this other X, for the
duration of the app'. The extra paramter method, if done type-safely,
introduces undesirable coupling between the caller and callee types.
qsort() achieves its genericity by foregoing type safety. i.e., in
order for it to be ignorant of the types it is manipulating it takes
untyped (void *) arguments. There is nothing to prevent someone from
calling qsort() on an array of apples and passing a pointer to a
function that compares oranges!
An example of this typeless mechanism you'll frequently see is the
'apply' function in collections. The purpose of an apply function is
to allow a developer to pass a callback to a collection and have it
be 'applied' to (called on) each item in the collection.
Unfortunately it often looks like this:
void apply(void (*func)(T &theItem,void *extraStuff),void *theStuff);
Chances are really good you don't have a function like func sitting
around, so you'll have to write one (lots of casting required). And
make sure you pass it the right stuff. Ugh.
Single Rooted Hierarchy
Beware of callback mechanisms that appear type safe but are in fact
not. These mechanisms usually involve some base-of-all-classes like
Object or EventHandler, and utilize casts from
ptr-to-member-of-derived to ptr-to-member-of-base. Experience has
indicated that single-rooted systems are unworkable if components are
to come from multiple sources.
Parameterize the Caller
The component designer could parameterize the component on the type
of the callee. Such parameterization is inappropriate in many
situations and callbacks are one of them. Consider:
class Button{
public:
virtual void click();
//...
};
template
class ButtonThatCallsBack:public class Button{
public:
ButtonThatCalls(T *who,void (T::*func)(void)):
callee(who),callback(func){}
void click()
{
(callee->*callback)();
}
private:
T *callee;
void (T::*callback)(void);
};
class CDPlayer{
public:
void play();
//...
};
//Connect a CDPlayer and a Button
CDPlayer cd;
ButtonThatCallsBack button(&cd,&CDPlayer::play);
button.click(); //calls cd.play()
A ButtonThatCallsBack would thus 'know' about CDPlayer and
provides an interface explicitly based on it. The problem is that
this introduces rigidity in the system in that the callee type
becomes part of the caller type, i.e. it is 'type-intrusive'. All
code that creates ButtonThatCallsBack objects must be made aware of
the callee relationship, increasing coupling in the system. A
ButtonThatCallsBack is of a different type than a
ButtonThatCallsBack, thus preventing by-value manipulation.
If a component has many callback relationships it quickly becomes
unworkable to parameterize them all. Consider a Button that wants to
maintain a dynamic list of callees to be notified upon a click event.
Since the callee type is built into the Button class type, this list
must be either homogeneous or typeless.
Library code cannot even create ButtonThatCallsBack objects because
their instantiation depends on application types. This is a severe
constraint. Consider GUI library code that reads a dialog description
from a resource file and creates a Dialog object. How can it know
that you want the Buttons in that Dialog to call back CDPlayers? It
can't, therefore it can't create the Buttons for you.
Callee Mix-In
The caller component designer can invent an abstract base class to be
the target of the callback, and indicate to application developers
that they mix-in this base in order to connect their class with the
component. I call this the "callee mix-in."
Here the designer of the Button class wants to offer a click
notification callback, and so defines a nested class Notifiable with
a pure virtual function notify() that has the desired signature.
Clients of the Button class will have to pass to its constructor a
pointer to a Notifiable, which the Button will use (at some point
later on) for notification of clicks:
class Button{
public:
class Notifiable{
public:
virtual void notify()=0;
};
Button(Notifiable *who):callee(who){}
void click()
{callee->notify();}
private:
Notifiable *callee;
};
Given :
class CDPlayer{
public:
void play();
//...
};
an application developer wishing to have a Button call back a
CDPlayer would have to derive a new class from both CDPlayer and
Button::Notifiable, overriding the pure virtual function to do the
desired work:
class MyCDPlayer:public CDPlayer,public Button::Notifiable{
public:
void notify()
{play();}
};
and use this class rather than CDPlayer in the application:
MyCDPlayer cd;
Button button(&cd);
button.click(); //calls cd.play()
This mechanism is type safe, achieves the decoupling of Button and
CDPlayer, and is good magazine article fodder. It is almost useless
in practice, however.
The problem with the callee mix-in is that it, too, is
type-intrusive, i.e. it impacts the type of the callee, in this case
by forcing derivation. This has three major flaws. First, the use of
multiple inheritance, particularly if the callee is a callee of
multiple components, is problematic due to name clashes etc. Second,
derivation may be impossible, for instance if the application
designer gets CDPlayers from an unchangeable, untouchable API
(library designers note: this is a big problem with mix-in based
mechanisms in general). The third problem is best demonstrated.
Consider this version of CDPlayer:
class CDPlayer{
public:
void play();
void stop();
//...
};
It doesn't seem unreasonable to have an application where one Button
calls CDPlayer::play() and another CDPlayer::stop(). The mix-in
mechanism fails completely here, since it can only support a single
mapping between caller/callee/member-function, i.e. MyCDPlayer can
have only one notify().
CALLBACKS USING TEMPLATE FUNCTORS
When I first thought about the inter-component callback problem I
decided that what was needed was a language extension to support
'bound-pointers', special pointers representing information about an
object and a member function of that object, storable and callable
much like regular pointers to functions. ARM 5.5 commentary has a
brief explanation of why bound pointers were left out.
How would bound pointers work? Ideally you would initialize them with
either a regular pointer-to-function or a reference to an object and
a pointer-to-member-function. Once initialized, they would behave
like normal pointer-to-functions. You could apply the function call
operator() to them to invoke the function. In order to be suitable
for a callback mechanism, the information about the type of the
callee would _not_ be part of the type of the bound-pointer. It might
look something like this:
// Warning - NOT C++
class Fred{
public:
void foo();
};
Fred fred;
void (* __bound fptr)() = &fred.foo;
Here fptr is a bound-pointer to a function that takes no arguments
and returns void. Note that Fred is not part of fptr's type. It is
initialized with the object fred and a
pointer-to-member-function-of-Fred, foo. Saying:
fptr();
would invoke foo on fred.
Such bound-pointers would be ideal for callbacks:
// Warning - NOT C++
class Button{
public:
Button(void (* __bound uponClickDoThis)() )
:notify(uponClickDoThis)
{}
void click()
{
notify();
}
private:
void (* __bound notify)();
};
class CDPlayer{
public:
void play();
};
CDPlayer cd;
Button button(&cd.play);
button.click(); //calls cd.play()
Bound-pointers would require a non-trivial language extension and
some tricky compiler support. Given the extreme undesirability of any
new language features I'd hardly propose bound-pointers now.
Nevertheless I still consider the bound-pointer concept to be the
correct solution for callbacks, and set out to see how close I could
get in the current and proposed language. The result is the Callback
library described below. As it turns out, the library solution can
not only deliver the functionality shown above (albeit with different
syntax), it proved more flexible than the language extension would
have been!
Returning from the fantasy world of language extension, the library
must provide two things for the user. The first is some construct to
play the role of the 'bound-pointer'. The second is some method for
creating these 'bound-pointers' from either a regular
pointer-to-function or an object and a pointer-to-member-function.
In the 'bound-pointer' role we need an object that behaves like a
function. Coplien has used the term functor to describe such objects.
For our purposes a functor is simply an object that behaves like a
pointer-to-function. It has an operator() (the function call
operator) which can be used to invoke the function to which it
points. The library provides a set of template Functor classes. They
hold any necessary callee data and provide pointer-to-function like
behavior. Most important, their type has no connection whatsoever to
the callee type. Components define their callback interface using the
Functor classes.
The construct provided by the library for creating functors is an
overloaded template function, makeFunctor(), which takes as arguments
the callee information (either an object and a
ptr-to-member-function, or a ptr-to-function) and returns something
suitable for initializing a Functor object.
The resulting mechanism is very easy to use. A complete example:
#include //include the callback library header
#include
class Button{
public:
Button(const Functor0 &uponClickDoThis)
:notify(uponClickDoThis)
{}
void click()
{
notify(); //a call to operator()
}
private:
Functor0 notify; //note - held by value
};
//Some application stuff we'd like to connect to Button:
class CDPlayer{ public:
void play(){cout<<"Playing"<
Functor2
Functor3
Functor4
Functor0wRet