Polymorphic Function Objects: by Stephen C. Dewhurst
Polymorphic Function Objects: by Stephen C. Dewhurst
Polymorphic Function Objects: by Stephen C. Dewhurst
by Stephen C. Dewhurst
C++ programmers are a loyal bunch. Whenever we’re faced with a situation that requires a feature the
language doesn’t support, we don’t abandon C++ to flirt with some other language; we just extend C++ to
support the feature in which we’re interested.
C++ is designed for this type of extension. Not only does it support the usual basic requirements of any
language that allows the creation of abstract data types (encapsulation and access protection), it also
provides the ability to overload operators, and to define initialization, destruction, copy, and conversion
semantics.
It is also important that C++ (and, presumably, its creator Bjarne Stroustrup) elects not to push a fixed
philosophy of what constitutes a well-written program or a good design. These issues have wisely been
left to the community of C++ programmers as a whole. C++ is a relatively complex language, and this
complexity is mitigated by the availability of C++ programming idioms and somewhat higher-level
design patterns that describe commonly accepted styles of implementing certain low and mid-level
designs. Since they are socially determined, these idioms evolve over time so as to be in sync with the
current needs of the C++ programming community. Less complex and less flexible languages, or
languages that come with a significant amount of “proper design” baggage, are invariably replaced in
short order by new languages that are more suitable to actual needs.
In this installment, we’ll look at the C++ “function object” idiom. As with other areas of the base
language--like pointers, arrays, and numeric types--we often find a need to extend the base language to
support a function or function pointer with augmented functionality.
Calling Back
Suppose you’re taking a long trip and I lend you my car for the purpose. Given the state of my car, I’ll
probably also hand you a sealed envelope with a telephone number in it, along with the instructions to
call the number in the envelope if you experience any engine problems. This is a callback. You do not
have to know the number in advance (it might be the number of a good repair shop, a bus line, or the city
dump), and in fact you may never have to call the number. In effect, the task of handling the “engine
trouble” event has been partitioned between you (also known as the framework) and me (also known as
the client of the framework). You know when it’s time to do something, but not what to do. I know what
to do if a particular event occurs, but not when to do it. Together we make an application.
A more prosaic example of the use of a callback than an exploding internal combustion engine might be a
GUI button.
typedef void (*Action)();
Dewhurst — Polymorphic Function Objects 2
class Button {
public:
Button( const char *label );
~Button();
void press() const;
void setAction( Action );
private:
string _label;
Action _action;
};
1
Gamma, et al., Design Patterns, Addison Wesley 1995, p. 233.
Dewhurst — Polymorphic Function Objects 5
class LaunchMissle : public Action {
public:
LaunchMissle( Location & );
void operator ()();
private:
Location &_deliveryLocation;
};
struct Abort : public Action {
void operator ()()
{ abort(); }
};
//…
SendFlowers sendMeFlowers( myHouse );
doit.setAction( sendMeFlowers );
2
Ibid., p. 163.
Dewhurst — Polymorphic Function Objects 6
}
private:
list<Action *> _a;
};
I don’t know about you, but I sometimes like to beep after executing a callback. I also get the occasional
urge to record callback results in a log file. Unfortunately, producing custom Actions like
LaunchMissile, LaunchMissileBeep, LaunchMissileLog, and
LaunchMissileLogBeep can be tedious. Instead, I’ll allow an Action to be decorated.3
class ActionDec : public Action {
public:
ActionDec( Action *toDecorate ) : a_( toDecorate ) {}
void operator ()() = 0;
private:
Action *a_;
};
3
Ibid., p. 175.
4
Ibid., p. 117.
Dewhurst — Polymorphic Function Objects 7
virtual ~Action();
virtual void operator ()() = 0;
virtual Action *clone() const = 0;
};
Button, and that no other contexts were using that Action in a way that would adversely affect the
behavior of the Button. A much safer alternative would be to set a Button’s callback to a copy of the
argument to setAction. The clone function allows us to do this while remaining happily ignorant of
the details of the Action we copy.
class Button {
public:
Button( const char *label );
~Button();
void press() const;
void setAction( const Action * );
const Action *getAction() const;
private:
string _label;
Action *_action;
};
Button::~Button()
{ delete _action; }
Functional Observations
I like to use callbacks to motivate function objects for a number of reasons. First, “everyone knows” how
to implement a callback--with a function pointer--and “everyone” is wrong. In applying a simple
reification of a function into an object we are able to overcome the obvious deficits of function pointers in
a simple and straightforward way.
The real benefit of the reification, however, lies in our ability to plug our function objects into the
Dewhurst — Polymorphic Function Objects 11
generative power afforded by many of the design patterns and C++ idioms defined for objects. Like most
successful complex designs, our Action hierarchy is the result of the interaction of a small number of
simple designs. This allows the hierarchy to be described simply, understood readily, and maintained and
modified easily.
Another benefit of the approach of composing pattern and idiom is that often these techniques can be
applied independently and in ignorance of each other. As the purveyor of a framework, we may provide
only the Button and Action classes. Users of the framework complete the application by providing
concrete Actions. Some of these actions may compose other actions with a Composite, or augment
them with a Decorator. If both composite and decorated actions happen to be present in a particular
customization of our framework, we can compose beeping launching logging efflorescent aborts.
The reification afforded by the use of function objects also allows us to unify what are really very
different parts of the C++ language under a common abstraction. In the case of the numerical integration
we examined above, we wanted to treat both pointers to nonmember functions and pointers to member
functions as essentially the same. Use of a polymorphic function object hierarchy allowed us to achieve
this unification easily and effectively.
end of article