Callbacks in C++ Using Template Functors

Download as pdf or txt
Download as pdf or txt
You are on page 1of 15

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
CALLBACKS IN C++ USING TEMPLATE FUNCTORS http://www.tutok.sk/fastgl/callback.html
1 of 15 05/05/2014 22:32
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
CALLBACKS IN C++ USING TEMPLATE FUNCTORS http://www.tutok.sk/fastgl/callback.html
2 of 15 05/05/2014 22:32
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();
//...
CALLBACKS IN C++ USING TEMPLATE FUNCTORS http://www.tutok.sk/fastgl/callback.html
3 of 15 05/05/2014 22:32
};
template <class T>
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<CDPlayer> button(&cd,&CDPlayer::play);
button.click(); //calls cd.play()
A ButtonThatCallsBack<CDPlayer> 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<X> is of a different type than a ButtonThatCallsBack<Y>, 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();}
CALLBACKS IN C++ USING TEMPLATE FUNCTORS http://www.tutok.sk/fastgl/callback.html
4 of 15 05/05/2014 22:32
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:
CALLBACKS IN C++ USING TEMPLATE FUNCTORS http://www.tutok.sk/fastgl/callback.html
5 of 15 05/05/2014 22:32
// 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.
CALLBACKS IN C++ USING TEMPLATE FUNCTORS http://www.tutok.sk/fastgl/callback.html
6 of 15 05/05/2014 22:32
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 <callback.h> //include the callback library header
#include <iostream.h>
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"<<endl;}
void stop(){cout<<"Stopped"<<endl;}
};
void wow()
{cout<<"Wow!"<<endl;}
void main()
{
CDPlayer cd;
//makeFunctor from object and ptr-to-member-function
Button playButton(makeFunctor(cd,&CDPlayer::play));
Button stopButton(makeFunctor(cd,&CDPlayer::stop));
//makeFunctor from pointer-to-function
Button wowButton(makeFunctor(&wow));
playButton.click(); //calls cd.play()
stopButton.click(); //calls cd.stop()
wowButton.click(); //calls wow()
}
Voila! A component (Button) has been connected to application objects and functions it knows nothing
about and that know nothing about Button, without any custom coding, derivation or modification of the
objects involved. And it's type safe.
The Button class designer specifies the callback interface in terms of Functor0, a functor that takes no
arguments and returns void. It stores the functor away in its member notify. When it comes time to call
back, it simply calls operator() on the functor. This looks and feels just like a call via a pointer-
to-function.
Connecting something to a component that uses callbacks is simple. You can just initialize a Functor with
the result of an appropriate call to makeFunctor(). There are two flavors of makeFunctor(). You can
call it with a ptr-to-stand-alone function:
makeFunctor(&wow)
CALLBACKS IN C++ USING TEMPLATE FUNCTORS http://www.tutok.sk/fastgl/callback.html
7 of 15 05/05/2014 22:32
OR with an object and a pointer-to-member function:
makeFunctor(cd,&CDPlayer::play)
I must come clean at this point, and point out that the syntax above for makeFunctor() is possible only in
the proposed language, because it requires template members (specifically, the Functor constructors
would have to be templates). In the current language the same result can be achieved by passing to
makeFunctor() a dummy parameter of type ptr-to-the-Functor-type-you-want-to-create. This iteration of
the callback library requires you pass makeFunctor() the dummy as the first parameter. Simply cast 0 to
provide this argument:
makeFunctor((Functor0 *)0,&wow)
makeFunctor((Functor0 *)0,cd,&CDPlayer::play);
I will use this current-language syntax from here on.
The Button class above only needs a callback function with no arguments that returns void. Other
components may want to pass data to the callback or get a return back. The only things distinguishing one
functor from another are the number and types of the arguments to operator() and its return type, if any.
This indicates that functors can be represented in the library by (a set of) templates:
//Functor classes provided by the Callback library:
Functor0 //not a template - nothing to parameterize
Functor1<P1>
Functor2<P1,P2>
Functor3<P1,P2,P3>
Functor4<P1,P2,P3,P4>
Functor0wRet<RT>
Functor1wRet<P1,RT>
Functor2wRet<P1,P2,RT>
Functor3wRet<P1,P2,P3,RT>
Functor4wRet<P1,P2,P3,P4,RT>
These are parameterized by the types of their arguments (P1 etc) and return value (RT) if any. The
numbering is necessary because we can't overload template class names on number of parameters. 'wRet'
is appended to distinguish those with return values. Each has an operator() with the corresponding
signature, for example:
template <class P1>
class Functor1{
public:
void operator()(P1 p1)const;
//...
};
template <class P1,class P2,class RT>
class Functor2wRet{
public:
RT operator()(P1 p1,P2 p2)const;
//...
};
These Functor classes are sufficient to meet the callback needs of component designers, as they offer a
standard and consistent way to offer callback services, and a simple mechanism for invoking the callback
function. Given these templates in the library, a component designer need only pick one with the correct
number of arguments and specify the desired types as parameters. Here's the DataEntryField that wants
a validation callback that takes a const String & and returns a Boolean:
#include <callback.h>
class DataEntryField{
CALLBACKS IN C++ USING TEMPLATE FUNCTORS http://www.tutok.sk/fastgl/callback.html
8 of 15 05/05/2014 22:32
public:
DataEntryField(const Functor1wRet<const String &,Boolean> &v):
validate(v){}
void keyHit(const String & stringSoFar)
{
if(validate(stringSoFar))
// process it etc...
}
private:
Functor1wRet<const String &,Boolean> validate;
//validate has a
//Boolean operator()(const String &)
};
These trivial examples just scratch the surface of what you can do given a general purpose callback library
such as this. Consider their application to state machines, dispatch tables etc.
The callback library is 100% compile-time type safe. (Where compile time includes template-instantiation
time). If you try to make a functor out of something that is not compatible with the functor type you will
get a compiler error. All correct virtual function behavior is preserved.
The system is also type flexible. You'll note that throughout this article I have said 'type compatible' rather
than 'exactly-matching' when talking about the relationship between the callback function and the callee
function. Experience has shown that requiring an exact match makes callbacks too rigid for practical use.
If you have done much work with pointer-to-function based interfaces you've probably experienced the
frustration of having a pointer to a function 'that would work' yet was not of the exact type required for a
match.
To provide flexibility the library supports building a functor out of a callee function that is 'type
compatible' with the target functor - it need not have an exactly matching signature. By type compatible I
mean a function with the same number of arguments, of types reachable from the functor's argument types
by implicit conversion. The return type of the function must be implicitly convertible to the return type of
the functor. A functor with no return can be built from a function with a return - the return value is safely
ignored.
//assumes Derived publicly derived from Base
void foo(Base &);
long bar(Derived &);
Functor1<Derived&> f1 =
makeFunctor((Functor1<Derived&> *)0,&foo);
//ok - will implicitly convert
f1 = makeFunctor((Functor1<Derived&> *)0,&bar);
//ok - ignores return
Any necessary argument conversions or ignoring of returns is done by the compiler, i.e. there is no
coercion done inside the mechanism or by the user. If the compiler can't get from the arguments passed to
the functor to the arguments required by the callee function, the code is rejected at compile time. By
allowing the compiler to do the work we get all of the normal conversions of arguments - derived to base,
promotion and conversion of built-in types, and user-defined conversions.
The type-flexibility of the library is something that would not have been available in a language extension
rendition of bound pointers.
Rounding out the functionality of the Functor classes are a default constructor that will also accept 0 as
an initializer, which puts the Functor in a known 'unset' state, and a conversion to Boolean which can be
used to test whether the Functor is 'set'. The Functor classes do not rely on any virtual function behavior
to work, thus they can be held and copied by-value. Thus a Functor has the same ease-of-use as a regular
pointer-to-function.
CALLBACKS IN C++ USING TEMPLATE FUNCTORS http://www.tutok.sk/fastgl/callback.html
9 of 15 05/05/2014 22:32
At this point you know everything you need to use the callback library. All of the code is in one file,
callback.h. To use a callback in a component class, simply instantiate a Functor with the desired
argument types. To connect some stuff to a component that uses Functors for callbacks, simply call
makeFunctor() on the stuff. Easy.
Power Templates
As usual, what is easy for the user is often tricky for the implementor. Given the black-box descriptions
above of the Functor classes and makeFunctor() it may be hard to swallow the claims of type-safety,
transparent conversions, correct virtual function behavior etc. A look behind the curtain reveals not only
how it works, but also some neat template techniques. Warning: most people find the pointer-to-member
and template syntax used in the implementation daunting at first.
Obviously some sort of magic is going on. How can the Functor class, with no knowledge of the type or
signature of the callee, ensure a type safe call to it, possibly with implicit conversions of the arguments? It
can't, so it doesn't. The actual work must be performed by some code that knows both the functor callback
signature and everything about the callee. The trick is to get the compiler to generate that code, and have
the Functor to point to it. Templates can help out all around.
The mechanism is spread over three components - the Functor class, a Translator class, and the
makeFunctor() function. All are templates.
The Functor class is parameterized on the types of the callback function signature, holds the callee data
in a typeless manner, and defines a typed operator() but doesn't actually perform the work of calling
back. Instead it holds a pointer to the actual callback code. When it comes time to call back, it passes the
typeless data (itself actually), as well as the callback arguments, to this pointed-to function.
The Translator class is derived from Functor but is parameterized on both the Functor type _and_ the
callee types. It knows about everything, and is thus able to define a fully type-safe static 'thunk' function
that takes the typeless Functor data and the callback arguments. It constructs its Functor base class with
a pointer to this static function. The thunk function does the work of calling back, turning the typeless
Functor data back into a typed callee and calling the callee. Since the Translator does the work of
converting the callee data to and from untyped data the conversions are considered 'safe'. The
Translator isA Functor, so it can be used to initialize a Functor.
The makeFunctor() function takes the callee data, creates a Translator out of it and returns the
Translator. Thus the Translator object exists only briefly as the return value of makeFunctor(), but
its creation is enough to cause the compiler to lay down the static 'thunk' function, the address of which is
carried in the Functor that has been initialized with the Translator.
All of this will become clearer with the details.
For each of the 10 Functor classes there are 2 Translator classes and 3 versions of makeFunctor().
We'll examine a slice of the library here, Functor1 and its associated Translators and makeFunctors.
The other Functors differ only in the number of args and return values.
The Functors
Since the Functor objects are the only entities held by the caller, they must contain the data about the
callee. With some care we can design a base class which can hold, in a typeless manner, the callee data,
regardless of whether the callee is a ptr-to-function or object/ptr-to-member-function combo:
//typeless representation of a function or object/mem-func
class FunctorBase{
public:
typedef void (FunctorBase::*_MemFunc)();
CALLBACKS IN C++ USING TEMPLATE FUNCTORS http://www.tutok.sk/fastgl/callback.html
10 of 15 05/05/2014 22:32
typedef void (*_Func)();
FunctorBase():callee(0),func(0){}
FunctorBase(const void *c,const void *f,size_t sz)
{
if(c) //must be callee/memfunc
{
callee = (void *)c;
memcpy(memFunc,f,sz);
}
else //must be ptr-to-func
{
func = f;
}
}
//for evaluation in conditions
//will be changed to bool when bool exists
operator int()const{return func||callee;}
class DummyInit{
};
////////////////////////////////////////////////////////////////
// Note: this code depends on all ptr-to-mem-funcs being same size
// If that is not the case then make memFunc as large as largest
////////////////////////////////////////////////////////////////
union{
const void *func;
char memFunc[sizeof(_MemFunc)];
};
void *callee;
};
All Functors are derived (protected) from this base. FunctorBase provides a constructor from typeless
args, where if c is 0 the callee is a pointer-to-function and f is that pointer, else c is pointer to the callee
object and f is a pointer to a pointer-to-member function and sz is that ptr-to-member-function's size (in
case an implementation has pointer-to-members of differing sizes). It has a default constructor which inits
to an 'unset' state, and an operator int to allow for testing the state (set or unset).
The Functor class is a template. It has a default constructor and the required operator() corresponding
to its template parameters. It uses the generated copy constructor and assignment operators.
/************************* one arg - no return *******************/
template <class P1>
class Functor1:protected FunctorBase{
public:
Functor1(DummyInit * = 0){}
void operator()(P1 p1)const
{
thunk(*this,p1);
}
FunctorBase::operator int;
protected:
typedef void (*Thunk)(const FunctorBase &,P1);
Functor1(Thunk t,const void *c,const void *f,size_t sz):
FunctorBase(c,f,sz),thunk(t){}
private:
Thunk thunk;
};
The Functor class has a protected constructor that takes the same typeless args as FunctorBase, plus an
additional first argument. This argument is a pointer to function (the thunk function) that takes the same
arguments as the operator(), plus an additional first argument of type const FunctorBase &. The
Functor stores this away (in thunk) and implements operator() by calling thunk(), passing itself and
the other arguments. Thus it is this thunk() function that does the work of 'calling back'.
CALLBACKS IN C++ USING TEMPLATE FUNCTORS http://www.tutok.sk/fastgl/callback.html
11 of 15 05/05/2014 22:32
A key issue at this point is whether operator() should be virtual. In the first iteration of my mechanism
the Functor classes were abstract and the operator()'s pure virtual. To use them for callbacks a set of
derived template classes parameterized on the callee type was provided. This required that functors
always be passed and held by reference or pointer and never by value. It also required the caller
component or the client code maintain the derived object for as long as the callback relationship existed. I
found the maintenance and lifetime issues of these functor objects to be problematic, and desired by-value
syntax.
In the current mechanism the Functor classes are concrete and the operator() is non-virtual. They can
be treated and used just like ptr-to-functions. In particular, they can be stored by value in the component
classes.
The Translators
Where does the thunk() come from? It is generated by the compiler as a static member of a template
'translator' class. For each Functor class there are two translator classes, one for stand-alone functions
(FunctionTranslator) and one for member functions (MemberTranslator). The translator classes are
parameterized by the type of the Functor as well as the type(s) of the callee. With this knowledge they
can, in a fully type-safe manner, perform two important tasks.
First, they can initialize the Functor data. They do this by being publicly derived from the Functor. They
are constructed with typed callee information and which they pass (untyped) to the functor's protected
constructor.
Second, they have a static member function thunk(), which, when passed a FunctorBase, converts its
callee data back into typed information, and executes the callback on the callee. It is a pointer to this
static function which is passed to the Functor constructor.
template <class P1,class Func>
class FunctionTranslator1:public Functor1<P1>{
public:
FunctionTranslator1(Func f):Functor1<P1>(thunk,0,f,0){}
static void thunk(const FunctorBase &ftor,P1 p1)
{
(Func(ftor.func))(p1);
}
};
FunctionTranslator is the simpler of the two. It is parameterized by the argument type of the Functor
and some ptr-to-function type (Func). Its constructor takes an argument of type Func and passes it and a
pointer to its static thunk() function to the base class constructor. The thunk function, given a
FunctorBase ftor, casts ftor's func member back to its correct type (Func) and calls it. There is an
assumption here that the FunctorBase ftor is one initialized by the constructor (or a copy). There is no
danger of it being otherwise, since the functors are always initialized with matching callee data and thunk
functions. This is what is called a 'safe' cast, since the same entity that removed the type information also
re-instates it, and can guarantee a match. If Func's signature is incompatible with the call, i.e. if it cannot
be called with a single argument of type P1, then thunk() will not compile. If implicit conversions are
required the compiler will perform them. Note that if func has a return it is safely ignored.
template <class P1,class Callee, class MemFunc>
class MemberTranslator1:public Functor1<P1>{
public:
MemberTranslator1(Callee &c,const MemFunc &m):
Functor1<P1>(thunk,&c,&m,sizeof(MemFunc)){}
static void thunk(const FunctorBase &ftor,P1 p1)
{
Callee *callee = (Callee *)ftor.callee;
MemFunc &memFunc(*(MemFunc*)(void *)(ftor.memFunc));
(callee->*memFunc)(p1);
}
CALLBACKS IN C++ USING TEMPLATE FUNCTORS http://www.tutok.sk/fastgl/callback.html
12 of 15 05/05/2014 22:32
};
MemberTranslator is parameterized by the argument type of the Functor, some class type (Callee), and
some ptr-to-member-function type (MemFunc). Not surprisingly it's constructor is passed 2 arguments, a
Callee object (by reference) and a ptr-to-member-function, both of which are passed, along with the
thunk function, to the base class constructor. Once again, the thunk function casts the typeless info back
to life, and then calls the member function on the object, with the passed parameter.
Since the Translator objects are Functor objects, and fully 'bound' ones at that, they are suitable
initializers for their corresponding Functor, using the Functor's copy constructor. We needn't worry
about the 'chopping' effect since the data is all in the base class portion of the Translator class and there
are no virtual functions involved. Thus they are perfect candidates for the return value of
makeFunctor()!
The makeFunctor Functions
For each Functor class there are three versions of makeFunctor(), one for ptr-to-function and a const
and non-const version for the object/ptr-to-member-function pair.
template <class P1,class TRT,class TP1>
inline FunctionTranslator1<P1,TRT (*)(TP1)>
makeFunctor(Functor1<P1>*,TRT (*f)(TP1))
{
return FunctionTranslator1<P1,TRT (*)(TP1)>(f);
}
The function version is straightforward. It uses the dummy argument to tell it the type of the functor and
merely returns a corresponding FunctionTranslator. I mentioned above that the Func type parameter of
FunctionTranslator was invariably a ptr-to-function type. This version of makeFunctor() ensures that
by explicity specifying it as such.
template <class P1,class Callee,class TRT,class CallType,class TP1>
inline MemberTranslator1<P1,Callee,TRT (CallType::*)(TP1)>
makeFunctor(Functor1<P1>*,Callee &c,TRT (CallType::* const &f)(TP1))
{
typedef TRT (CallType::*MemFunc)(TP1);
return MemberTranslator1<P1,Callee,MemFunc>(c,f);
}
This is the gnarliest bit. Here makeFunctor is parameterized with the type of the argument to the
Functor, the type of the callee, the type of the class of which the member-function is a member, the
argument and return types of the member function. Whew! We're a long way from Stack<T> land! Like
the ptr-to-function version, it uses the dummy first argument of the constructor to determine the type of
the Functor. The second argument is a Callee object (by reference). The third argument is this thing:
TRT (CallType::* const &f)(TP1)
Here f is a reference to a constant pointer to a member function of CallType taking TP1 and returning
TRT. You might notice that pointer-to-member-functions are all handled by reference in the library. On
some implementations they can be expensive to pass by value and copy. The significant feature here is
that the function need not be of type pointer-to-member-of-Callee. This allows makeFunctor to match on
(and ultimately work with) a ptr-to-member-function of some base of Callee. It then typedefs that bit and
returns an appropriate MemberTranslator.
template <class P1,class Callee,class TRT,class CallType,class TP1>
inline MemberTranslator1<P1,const Callee,TRT (CallType::*)(TP1)const>
makeFunctor(Functor1<P1>*,const Callee &c,TRT (CallType::* const &f)(TP1)const)
{
typedef TRT (CallType::*MemFunc)(TP1)const;
return MemberTranslator1<P1,const Callee,MemFunc>(c,f);
}
CALLBACKS IN C++ USING TEMPLATE FUNCTORS http://www.tutok.sk/fastgl/callback.html
13 of 15 05/05/2014 22:32
This last variant just ensures that if the Callee is const the member function is also (note the const at the
end of the third argument to the constructor - that's where it goes!).
That, for each of ten Functors, is the whole implementation.
Can Your Compiler Do This?
The callback library has been successfully tested with IBM CSet++ 2.01, Borland C++ 4.02 (no, its not
twice as good ;-), and Watcom C++32 10.0. It is ARM compliant with the exception of expecting trivial
conversions of template function arguments, which is the behavior of most compilers. I am interested in
feedback on how well it works with other implementations.
Summary
Callbacks are a powerful and necessary tool for component based object-oriented development in C++.
They can be a tremendous aid to the interoperability of libraries. The template functor system presented
here meets all the stated criteria for a good callback mechanism - it is object-oriented, compile-time
type-safe, generic, non-type-intrusive, flexible and easy to use. It is sufficiently general to be used in any
situation calling for callbacks. It can be implemented in the current language, and somewhat more
elegantly in the proposed language.
This implementation of callbacks highlights the power of C++ templates - their type-safety, their
code-generation ability and the flexibility they offer by accepting ptr-to-function and ptr-to-member-
function type parameters.
Ultimately the greatest benefit is gained when class libraries start using a standard callback system. If
callbacks aren't in the components, they can't be retrofitted. Upon publication of this article I am making
this Callback library freely available in the hope that it will be adopted by library authors and serve as a
starting point for discussion of a standard callback system.
References
Stroustrup, B. The Design and Evolution of C++, Addison-Wesley, Reading, MA 1994
Coplien, J.O. Advanced C++ Programming Styles and Idioms, Addison-Wesley, Reading, MA 1992
Ellis, M.A. and B. Stroustrup. The Annotated C++ Reference Manual, Addison-Wesley, Reading, MA
1990
Lippman, S.B. C++ Primer 2nd Edition, Addison-Wesley, Reading, MA 1991
Acknowledgments
Thanks to my fellow developers at RCS and to Greg Comeau for reviewing and commenting on this
article.
About the Author
Rich is Technical Design Lead at Radio Computing Services, a leading software vendor in the radio
industry. He designed and teaches the Advanced C++ course at New York University's Information
Technologies Institute.
He can be reached at:
[email protected]
CALLBACKS IN C++ USING TEMPLATE FUNCTORS http://www.tutok.sk/fastgl/callback.html
14 of 15 05/05/2014 22:32
Home
CALLBACKS IN C++ USING TEMPLATE FUNCTORS http://www.tutok.sk/fastgl/callback.html
15 of 15 05/05/2014 22:32

You might also like