3. Frameworks, Toolkits, and Polymorphsim
Overview
Polymorphism allows programmers to declare partially complete classes and functions. The
idea being to complete the declaration with a subclass or a template instantiation when
more application-specific information is available. Of course this is also the idea behind
frameworks, toolkits, and many design patterns. In each case application-independent logic
is captured by one part of the program, while application-dependent logic is concentrated in
another.
After a brief survey of frameworks and a more formal introduction to polymorphism,
fragments of a fictional music framework called MFW are presented as a pretext to
introducing virtual functions, abstract classes, templates, and several important design
patterns. These patterns and mechanisms are subsequently assembled to create a working
computer game framework called FUN (application frameworks will be developed in
Chapter 6) and a working toolkit called PIPES for assembling applications that instantiate
the Pipe and Filter architectural pattern.
Frameworks
A framework is a library of collaborating base classes that capture the logic, architecture,
and interfaces common to a family of similar applications. Frameworks are customized into
specific applications by deriving classes from framework classes.
Pattern Oriented Programming with C++/Pearce
A horizontal framework provides general services, and so can be customized into a wide
assortment of diverse applications. A vertical framework fixes a narrow application
domain and therefore requires less customization:
The idea of a partially completed application sounds strange at first. Why would anyone
want such a thing? But the idea makes sense for organizations that can't afford custom
software, and that require something beyond the general features of off-the-shelf software.
Typically, framework developers are not the application developers who customize the
framework. But if a framework for a particular application doesn't exist, it might make
sense for the application developers to create their own framework as a first step. In this
way a library of frameworks evolves that enables developers to quickly produce new
applications based on combinations of frameworks that have already been tested by
3-2
3. Frameworks, Kits, and Polymorphism
previous applications. This is called framework-based software development [ROG].
Framework-based development also makes sense for students, who can gain experience
writing challenging code without devoting too much time to application domain details.
Example: Application Frameworks
Although word processors, spread sheets, and database browsers don't have much in
common, they all have elaborate, platform-dependent graphical user interfaces, and they
can all save the user's work into a file or database. Application frameworks such as
Microsoft Foundation Classes (MFC), Object Windows Library (OWL), ET++, and JFC
provide generic graphical user interfaces with windows, menus, toolbars, dialog boxes, and
other common user interface components. (Application frameworks are covered in detail in
Chapter 7.)
Example: Expert System Shells
Expert systems are interactive programs that automate the collection, representation, and
generation of knowledge in a specific application domain. Expert systems are used by
doctors to diagnose patients (MYCIN), by geologists to locate good places to drill for oil
3-3
Pattern Oriented Programming with C++/Pearce
(PROSPECTOR), and by engineers to configure computers (XCON). These applications
are different, but they have common features such as user interfaces, inference engines, and
schemes for representing and organizing knowledge (semantic networks, frames, scripts,
rules, etc.). These common features can be captured in an expert system framework
(more commonly called an expert system shell). Examples include ART (Automated
Reasoning Tool by Inference Corporation), KEE (Knowledge Engineering Environment by
IntelliCorp), and GURU (by Micro Data Base Systems). See [FIRE] for a survey of expert
systems.
Example: Client-Server Frameworks
In a client-server application shared data is maintained by a program called the server, and
presented and manipulated by programs called clients. Often machine or network
boundaries separate clients and servers. The World Wide Web (WWW) is a typical client
server application. Web servers maintain collections of web pages that are delivered upon
request to web clients (i.e., web browsers).
In fact, WWW is such a generic client-server application, that Java has created a client-
server framework that piggy backs on top of WWW. An applet is a customizable client
3-4
3. Frameworks, Kits, and Polymorphism
that is executed by a web client. The applet communicates with a customizable server
called a servlet that is executed by a web server. Applets and Servlets are documented on
the web at [WWW 7].
Example: Workflow Frameworks
A workflow is a routine business or engineering process. For example, the workflow of a
grocery store check-out clerk might be described by the following state diagram:
3-5
Pattern Oriented Programming with C++/Pearce
A workflow application is a program that guides a worker through a workflow. For each
state, the workflow application prompts the worker for the information required to
complete the state. This may involve fetching and updating records in various remote
databases. When the information is collected, the application determines which state to
move to next.
For example, a help desk application guides a customer service agent through a sequence of
diagnostic questions and tests; a warehouse inventory management system guides
warehouse workers through an order processing workflow; and a point-of-sale application
guides check-out clerks through the states described above. When a sale is completed, the
inventory database is updated, any accounting software is notified, and the total amount in
the cash register is recomputed.
Although these applications are different, they have a much in common that can be
captured in a workflow framework. Each consists of a number of well defined
transactions. A graphical user interface displays each transaction and provides controls for
completing, canceling, undoing, redoing, saving, and restoring the transaction. Information
gathered during a transaction determines the next transaction.
3-6
3. Frameworks, Kits, and Polymorphism
IBM San Francisco is a family of workflow frameworks written in Java that can be
customized into a variety of business management systems. Currently, IBM San Francisco
offers three frameworks: a general ledger framework that provides the core functionality
used by most accounting applications, including budgeting, accounts receivable, and
accounts payable; a warehouse management framework which provides warehouse
control, picking stock, reception, and shipment processes needed by most warehouse
management systems; and an order management framework which provides the business
logic required in many manufacturing applications, including sales orders, purchase orders,
and pricing. (Documentation about IBM San Francisco can be found on the web at [WWW
10].)
Polymorphism
A type system for a language L is a set of primitive types together with a set of rules for
constructing, comparing, and naming types, as well as rules for binding types to the
expressions and values of L. For example, the primitive types of C++ include int, float,
char, and bool. Arrays, structs, unions, and pointers are examples of constructed types.
3-7
Pattern Oriented Programming with C++/Pearce
Instances of a monomorphic or uniform type all have the same representation, while
instances of a polymorphic or multi-form type can have a variety of representations. For
example, a monomorphic Real type might represent all real numbers using the IEEE
floating point standard representation, but a polymorphic Complex type might represent
some complex numbers using polar coordinate representation (reit) and others using
rectangular coordinate representation (a+bi). A polymorphic type often can be viewed as a
family of logically related subtypes.
In C++ an abstract class is a class with one or more pure virtual functions. In the following
example, Shape is an abstract class with a pure virtual draw() function:
class Shape{public:
virtual void draw() = 0;// etc.
};
Abstract classes can be regarded as polymorphic types1 in the sense that instances of
concrete shape-derived classes can be treated as generic shapes. For example, although we
can't declare instances of the Shape class, we can declare Shape pointers that can point at
any instance of a concrete Shape subclasses:
Shape* s[3];s[0] = new Triangle();s[1] = new Circle();s[2] = new Rectangle();for(int i = 0; i < 3; i++) s[i]->draw();
A C++ class template defines a paremeterized family of types, and therefore can also be
regarded as a polymorphic type (although this is somewhat at odds with C++ terminology).
For example, the list<T> template in the standard template library defines a family of
types, including:
1 We should be more careful with our terminology. Technically, a type is a data representation scheme. A class is a type together with a collection of member functions, and an abstract data type (ADT) is a specification of a type in terms of member function prototypes and axioms.
3-8
3. Frameworks, Kits, and Polymorphism
list<string>list<Shape*>list<list<int> >etc.
The term "polymorphic" is also applied to functions, although a polymorphic function is
simply an instance of a polymorphic function type. More concretely, a polymorphic
function is a family of closely related functions. For example, the virtual Shape::draw()
function is a polymorphic function that represents the family of draw() functions defined in
the Shape-derived classes.
C++ allows programmers to define function templates:
typedef <typename T>void swap(T& x, T& y){
T temp = x;x = y;y = temp;
}
We can think of the swap() template as the polymorphic representative of the family of its
instances.
Even a family of functions that simply share a name:
double area(Triangle x) { return x.height * x.base / 2; }double area(Rectangle x) { return x.height * x.width; }double area(Circle x) { return pi * x.radius * x.radius; }
can be thought of as variants of a single polymorphic function.
Working with Unknown Classes
In a sense, a framework is a polymorphic application. A framework is an application that
can take on many forms, depending on how it is customized. As such, framework
programmers often need to refer to classes that won't be defined until the framework is
customized, and of course different customizations may define these classes in different
ways. Polymorphism allows framework programmers to work around these critical pieces
of missing information.
3-9
Pattern Oriented Programming with C++/Pearce
MFW: A Framework for Music Applications
Assume we are developing a music framework called MFW. MFW is a horizontal
framework that can be customized into various musical applications such as score editors,
virtual recording studios, virtual instruments, expert systems for composers, and interfaces
to computer controlled instruments such as synthesizers.
One part of MFW defines various ensembles of musical instruments: bands, orchestras,
trios, quartets, etc. Unfortunately, the types of musical instruments will be defined much
later in the various customizations of the framework. How can MFW form ensembles
without knowing the identity of their constituent instruments? There are two solutions:
make Instrument an abstract class in MFW, or make Instrument an MFW template
parameter.
MFW with Abstract Classes
Although the specific types of instruments may be unknown, MFW can define an abstract
Instrument base class:
class Instrument{public:
virtual void play() = 0;};
MFW is unable to implement the play() member function, so it is declared as a pure virtual
function. Derived class programmers will be required to provide implementations. Even
though MFW couldn't implement play(), it can still call play(). For example, here is the
MFW definition of Trio. Notice that Trio::play() calls the play() function of each
instrument:
3-10
3. Frameworks, Kits, and Polymorphism
class Trio{public:
Trio(Instrument *a = 0, Instrument *b = 0, Instrument *c = 0){
first = a;second = b;third = c;
}void play(){
if (first) first->play();if (second) second->play();if (third) third->play();
}private:
Instrument *first, *second, *third;};
Suppose a programmer customizing MFW wishes to create and play trios consisting of
different combinations of horns, harps, and drums. The first step is to create Instrument
derived classes. For example:
class Harp: public Instrument{public:
void play(){
cout << "plink, plink, plink\n";}
};
Here is how trios are created and played in the customization:
Trio t1(new Harp(), new Horn(), new Drum());Trio t2(new Drum(), new Drum(), new Harp());t1.play();t2.play();
Heterogeneous Containers
A container or collection is an object that stores other objects. Stacks and queues are
familiar examples of containers. A heterogeneous container stores objects of different
types. For example, an orchestra can be regarded as a container of different types of
instruments:
3-11
Pattern Oriented Programming with C++/Pearce
class Orchestra{public:
void add(Instrument* p) { instruments.push_back(p); }void play();
private:vector<Instrument*> instruments;
};
An orchestra plays by invoking the play() method of each of its instruments:
void Orchestra::play(){
for(unsigned i = 0; i < instruments.size(); i++)instruments[i]->play();
}
Clients of the orchestra class can add a variety of instruments to orchestra instances:
Orchestra orch;orch.add(new Horn());orch.add(new Harp());orch.add(new Horn());orch.add(new Harp());orch.add(new Drum());orch.play();
Unfortunately, orchestras can only hold Instrument pointers, not the instruments
themselves. Why?
Virtual Factory Methods
Let's enhance MFW by adding an abstract Note class. Each note encapsulates a frequency
and a duration:
class Note{public:
Note(double f = 100, double d = 300) { freq = f; duration = d; }virtual void play() = 0; // quality? timbre?
protected:double freq; // in Hzdouble duration; // in mSec
};
Of course any musician will tell you that a note played on a guitar sounds quite different
from the same note played on a horn. For this reason we have left the implementation of
play() to MFW customizations; for example:
3-12
3. Frameworks, Kits, and Polymorphism
class HornNote: public Note{public:
HornNote(double f = 100, double d = 300): Note(f, d) {}void play(){
cout << "honk\n";}
};
Now that we can talk about notes in MFW we can provide an implementation of the play()
function in the Instrument base class:
void Instrument::play() {
for(int i = 0; i < 3; i++){
Note* n = makeNote();n->play(); delete n;
}}
But how can the Instrument class know what type of Notes to create? It would seem that
creating an unknown type of object is more difficult than simply using an unknown type of
object. How much memory should be allocated? How should the memory be initialized?
Creating unknown objects is a common theme in programming. The various approaches to
this problem are summarized by the Factory Method design pattern:
Factory Method [Go4]
Other Names
Virtual constructor.
Problem
A "factory" class can't anticipate the type of "product" objects it must create.
Solution
Provide the factory class with an ordinary member function that creates product objects. This is called a factory method. The factory method can be a virtual function implemented in a derived class, or a template function parameterized by a product constructor.
There are three variations of the pattern: virtual, smart, and template factory methods.
Template factory methods will be discussed later in the chapter, while smart factory
methods will be discussed in Chapter 5.
3-13
Pattern Oriented Programming with C++/Pearce
An ordinary member function that creates and returns new objects is called a factory
method. Although C++ doesn't allow virtual constructors, factory methods can be virtual:
class Instrument{public:
void play();// virtual factory method:virtual Note* makeNote(double f = 100, double d = 100) = 0;
};
The obligation to implement the virtual factory method falls on the shoulders of derived
classes:
class Horn: public Instrument{public:
// factory method:Note* makeNote(double f = 100, double d = 100) {
return new HornNote(f, d); }
};
One problem associated with virtual factory methods is that programmers must maintain
two parallel inheritance hierarchies, one for factories, and one for products:
If a programmer creates a new type of note, he must remember to create the corresponding
factory instrument that creates the note.
3-14
3. Frameworks, Kits, and Polymorphism
MFW with Templates
Instead of introducing an abstract Instrument class, MFW could have used templates
parameterized by instruments. For example MFW could have defined a trio to be a
template parameterized by the types of instruments in the trio:
template <typename A, typename B, typename C>class Trio{public:
Trio(){
first = new A();second = new B();third = new C();
}void play(){
if (first) first->play();if (second) second->play();if (third) third->play();
}private:
A* first;B* second;C* third;
};
Notice that the template parameters not only parameterize the types of instruments in the
trio, they also parameterize the instrument constructors called by the Trio constructor.
Because there is no abstract Instrument base class, MFW customizers don't need to derive
their instrument classes:
class Drum{public:
void play() { cout << "thump, thump, thump\n"; }};
The customization specifies the types of instruments a trio contains as template arguments:
Trio<Horn, Harp, Drum> trio1;Trio<Drum, Drum, Harp> trio2;trio1.play();trio2.play();
3-15
Pattern Oriented Programming with C++/Pearce
Of course the compiler will complain if one of the template arguments doesn't have a
member function called play():
Trio<Horn, Harp, Airplane> trio3; // error, no Airplane::play()!
Homogeneous Containers
The type of objects stored in a template container is given by the template argument. For
example, an orchestra might consist of several sections: horns, drums, strings, etc. each
with varying numbers of a particular type of instrument:
template <typename Instrument>class Section{public:
void add(Instrument* p) { instruments.push_back(p); }void play();
private:vector<Instrument*> instruments;
};
Notice that sections are not heterogeneous collections. One section contains one type of
instrument. We could define a section of mixed instruments if we reintroduce the abstract
instrument base class for Horn, Drum, and Harp, then proceed as follows:
Section<Instrument*> band;band.addInstrument(new Horn());band.addInstrument(new Drum());// etc.
Template containers are the approach taken in the C++ standard template library, while
heterogeneous containers are the approach taken in Java, where all classes extend a
common abstract base class called Object.
Template Factory Methods
Returning to the factory method design pattern, we could have defined Factory as a
template and treated the instrument class as a template parameter. Actually, it's not just the
instrument class that is represented by the template parameter, it's also the instrument
constructor:
3-16
3. Frameworks, Kits, and Polymorphism
template <typename Note>class Instrument{public:
void play(){
for (int i = 0; i < 3; i++){
Note* n = makeNote();n->play(); delete n;
}}Note* makeNote() { return new Note(); }
};
MFW customizations no longer need to derive their note classes from an abstract Note base
class:
class HarpNote{public:
void play(){
cout << "plink\n";}
};
Also, MFW customizations no longer need to create Instrument-derived classes. A horn is
simply an instance of the Instrument<HornNote> class, while a harp is an instance of the
Instrument<HarpNote> class:
Instrument<HornNote> *a = new Instrument<HornNote>();Instrument<DrumNote> *b = new Instrument<DrumNote>();Instrument<HarpNote> *c = new Instrument<HarpNote>();a->play();b->play();c->play();
Toolkits
A toolkit (also called an abstract factory or simply a kit) is a class or object that provides
factory methods that produce the components needed to construct all or part of an
application. In a sense, toolkits are precursors to frameworks. While a framework is already
assembled and only requires customization, a toolkit only provides the components
programmers will need to assemble an application. The advantage of a toolkit is that the
entire assembly process can be described relative to the toolkit. Thus, the toolkit decouples
3-17
Pattern Oriented Programming with C++/Pearce
the assembly process from the details of how the components are created. Toolkits are
formalized by the abstract factory design pattern:
Abstract Factory [Go4]
Other Names
Kit, toolkit
Problem
A system should be independent of how its components are implemented.
Solution
Provide the system with an abstract factory parameter. An abstract factory is a class consisting of virtual or template factory methods for making the system's components. Various derived classes or template instances provide implementations of the factory methods.
As an example, let's consider building graphical user interfaces (GUIs). Unfortunately, the
standard C++ library doesn't provide GUI components such as windows, menus, and
buttons. Part of the reason for this is that GUI components are highly platform-dependent.
So instead, these components are supplied by platform-specific libraries such as the
X-Windows library. This creates a huge problem when its time to port an application to a
new platform. Studies show that a GUI might comprise 30% to 60% of a commercial
application's code, all of which must be rewritten for the port!
We can partially relieve the pain of a port by using a toolkit to decouple the construction of
a GUI from the details of how the GUI components are constructed. We begin by defining
an abstract user interface toolkit:
class UIToolkit{public:
virtual Window* makeWindow() = 0;virtual Button* makeButton() = 0;virtual MenuItem* makeMenuItem() = 0;virtual EditBox* makeEditBox() = 0;virtual ListBox* makeListBox() = 0;// etc.
};
In addition, the toolkit includes a hierarchy of abstract component classes or interfaces:
3-18
3. Frameworks, Kits, and Polymorphism
Here is a sketch of the component hierarchy's base class:2
class UIComponent {public:
// draw this component in parent window:virtual void draw() = 0;// handle keyboard & mouse messages:virtual void handle(Message msg) {}// etc.
protected:Point corner; // upper left cornerint height, width; // sizeWindow* parent; // = 0 for desktop window
};
The GUI for a particular application is constructed by a function parameterized by
UIToolkit:
Window* makeMyGUI(UIToolkit* tk){
Window* w = tk->makeWindow();Button* b = tk->makeButton();w->adopt(b);EditBox* e = tk->makeEditBox();w->adopt(e);// etc.return w;
}
We only need to describe the construction of our GUI once, in makeMyGUI(). If we want
to construct a GUI for a particular platform, we simply call makeMyGUI() with a platform-
specific implementation of UIToolkit.
2 UI components will be discussed in greater detail in Chapter 6.
3-19
Pattern Oriented Programming with C++/Pearce
For example, assume Z Windows is a library of GUI components for a particular platform
(e.g., Z = X, MFC, MacApp2, etc.):
Someone knowledgeable in Z Windows programming could provide implementations of
each of the UIToolkit interfaces. This can be done using adapters (which will be discussed
in Chapter 4). For example:
class ZButtonAdapter: public Button{public:
ZButtonAdapter() { peer = new ZButton(); }~ZButtonAdapter() { delete peer; }void draw() { peer->paint(); }void handle(Message msg) { peer->onMsg(msg); }
private:ZButton* peer; // the corresponding Z component
};
We must also provide a Z Windows implement the abstract toolkit class:
class ZUIToolkit: public UIToolkit{public:
virtual Window* makeWindow() { return new ZWindowAdapter(); }virtual Button* makeButton() { return new ZButtonAdapter(); }virtual MenuItem* makeMenuItem() { return new ZMenuItemAdapter(); }virtual EditBox* makeEditBox() { return new ZEditBoxAdapter(); }virtual ListBox* makeListBox() { return new ZListBoxAdapter(); }// etc.
};
Here is how the programmer might start an application using the ZUIToolkit:
3-20
3. Frameworks, Kits, and Polymorphism
int main(){
Window* appWindow = makeMyGUI(new ZUIToolkit());appWindow->draw(); // draw app window & start msg loopreturn 0;
};
Generic Methods
Sometimes the general sequence of tasks a function must perform is the same across a
variety of applications, suggesting that the function belongs in a common framework.
However, the tasks performed by the function are application-specific, suggesting the
function belongs in the various framework customizations.
The generic algorithm design pattern solves this problem. Move the function into the
framework and declare the application-specific tasks to be pure virtual functions in the
framework. A function like this is called a generic algorithm or a template method.3
Generic Algorithm [Go4], [ROG]
Other Names
Template method
Problem
Parts of an algorithm are invariant across a family of framework customizations, while other parts are not.
Solution
Move the algorithm into the framework. Replace the non-invariant parts by calls to virtual functions that can be implemented differently in different customizations.
Generic algorithms follow the inverted control structure sometimes called the Hollywood
principle: "Don't call us, we'll call you."
Let's start with a trivial example to clarify the idea; more serious examples will come later.
Suppose we want to add a symphony class to our music framework, MFW. Symphonies are
usually divided into four movements, so playing a symphony is easy: play the first
movement, then the second, then the third, finally the fourth. Of course the similarity
between symphonies ends here. The particulars of each movement vary from one
symphony to the next, and will have to be specified in various customizations of the
3 Warning: template methods are not the same as C++ template functions.
3-21
Pattern Oriented Programming with C++/Pearce
framework. We can still add an abstract symphony base class to our framework, but play()
will have to be a generic algorithm:
class Symphony{public:
void play() // a generic algorithm{
doFirstMovement();doSecondMovement();doThirdMovement();doFourthMovement();
}protected:
virtual void doFirstMovement() = 0;virtual void doSecondMovement() = 0;virtual void doThirdMovement() = 0;virtual void doFourthMovement() = 0;
};
Framework customizers will have to subclass Symphony and implement the do-functions.
For example:
class TheFifth: public Symphony{
void doFirstMovement(){
cout << "dah, dah, dah, duh ...\n";}void doSecondMovement(){
cout << "duh, duh, duh, dah ...\n";}void doThirdMovement(){
cout << "dah, duh, duh, dah ...\n";}void doFourthMovement(){
cout << "duh, dah, dah, duh ...\n";}
};
Singletons
Sometimes we want a class to have at most one instance. Why? Multiple instances might
lead to confusion. For example, it would be confusing if an application had multiple user
interfaces, simultaneously:
GUI gui1, gui2; // which one to use?
3-22
3. Frameworks, Kits, and Polymorphism
Multiple instances might lead to sharing problems. For example, changes to one instance of
a company's budget must be reflected in other instances:
Budget budget1, budget2; // must remember to keep synchronized
In some situations, multiple instances might simply be illogical:
TheFifth s1, s2, s3; // how many Fifth Symphonies did he write?
The Singleton pattern solves this problem by making all constructors private. A public
factory method is provided that allows users to create instances, but the factory method
always returns pointers to the same hidden instance:
Singleton [Go4]
Problem
There must be at most one instance of a class.
Solution
Make all constructors private, including the default and copy constructors. Provide a static function that initializes and returns a private, static pointer to the sole instance of the class.
But doesn't this create a chicken and egg problem? How do we get the first instance of the
class from which we can invoke the factory method:
y = x->makeSingleton(); // where does x come from?
This problem is solved by making the factory method static:
y = Singleton::makeSingleton(); // no implicit parameter needed
For example, let's apply the singleton pattern to the TheFifth class from the previous
example:
3-23
Pattern Oriented Programming with C++/Pearce
class TheFifth: public Symphony{public:
static TheFifth* makeTheFifth() // factory method{
if (!theFifth)theFifth = new TheFifth(); // constructor access ok here
return theFifth;}// etc.
private:static TheFifth* theFifth; // the one and onlyTheFifth() {} // hide default constructorTheFifth(const TheFifth& tf) {} // hide copy constructor~TheFifth() {} // hide destructor
};
Notice that the default and copy constructors are made private, as is the destructor. The
pointer to the one and only instances is declared to be a private static variable. It must be
defined and initialized separately:
TheFifth* TheFifth::theFifth = 0;
Unsuspecting clients can call the factory method as many times as they want, but only one
instances is ever created:
int main(){
TheFifth* s1 = TheFifth::makeTheFifth();TheFifth* s2 = TheFifth::makeTheFifth();TheFifth* s3 = TheFifth::makeTheFifth();s1->play();s2->play();s3->play();return 0;
}
Applying the delete operator to one pointer, which would turn the other pointers into
dangling references, is prevented by the compiler because the destructor is private:
delete s3; // error, cannot access private member
Any attempt to create instances by other means meets with a similar fate:
TheFifth s4(*s3); // error, cannot access private memberTheFifth s5; // error, cannot access private member
3-24
3. Frameworks, Kits, and Polymorphism
The compiler even forbids Fifth Symphony value parameters, because the mechanism for
passing value parameters—the copy constructor—has been declared private:
void play(TheFifth s) { s.play(); } // error
Although Fifth Symphony reference parameters are allowed:
void play(TheFifth& s) { s.play(); } // okay
FUN: A Framework for Adventure Games
Let's put the patterns we have learned together by building a framework for text-based
adventure games. Our framework will be called FUN. The restriction to text-based games
(yes, such things really did exist) is only because of the unavailability of graphics utilities
in the standard library, but the architecture of the framework could easily be used for
graphics-based games.
The framework is possible because most adventure games follow the same basic plot:
A hero wanders from room to room in a spooky maze. He/she searches for enough keys to unlock a door and escape from the maze. Unfortunately, monsters hiding in each room hamper the search by attacking the hero upon entry. Naturally, the hero fights back. All of this fighting depletes the energy of the hero and the energy of the monsters. Some rooms contain "energy bowls". Drinking from an energy bowl partially restores the hero's energy. The game ends when the hero escapes or when the hero dies (i.e., the hero's energy level reaches zero).
This plot can be captured in the FUN framework. The nature of the rooms, monsters, and
heroes is specified in various customizations of the framework. For example, here are some
excerpts from a Medieval customization of the FUN framework called Dungeons and
Dragons. The user controls the hero using commands for moving, fighting, drinking, and
other of life's basic necessities:
3-25
Pattern Oriented Programming with C++/Pearce
-> helpObjective: Find enough keys to unlock a room and escape.help (displays this message)quit (terminates session)check (describe yourself)drink (drink any energy bowls in room)fight (fight all monsters in room)grab (grab any keys in room)map (describe the maze)move DIR (move hero DIR = N, S, E, or W)scan (describe the room)super (super charge hero)->
The maze is an N by M grid of rooms. Thus, each room has two, three, or four neighboring
rooms. The game begins with the hero standing in the North-West corner room of the
maze:
When the hero moves into a new room, the room is described. In a graphics-based game a
picture or animation of the room might be displayed in a window. If there are monsters in
the room, they attack and the hero defends:
-> move NThat door is locked-> move SThis is a dark, damp dungeon where prisoners rot (# of monsters = 1) (# of energy bowls = 5) (# of keys = 1) (# of locks = 26)A dragon flames Lancelot (damage = 3)Lancelot slashes at a dragon (damage = 8)done-> checkLancelot is a brave knight with a sharp sword (Lancelot.energy = 96) (Lancelot.keys = 0)done->
3-26
3. Frameworks, Kits, and Polymorphism
Notice that the hero's energy level has dropped from 100%, the maximum, to 96% as a
result of the fighting.
In addition to monsters, some rooms contain keys and energy bowls. The hero drinks from
the bowls and collects the keys:
-> drinkLancelot drinks 5 energy bowlsdone-> grabLancelot collects 1 keys.done->
When the hero moves into a room in which the number of locks is less than or equal to the
number of keys the hero has collected, the hero escapes and the game ends in a victory for
the user:
-> move nLancelot has escaped!
Of course in a space-age customization of FUN heroes are spacemen, rooms are labs, and
monsters are killer robots:
-> move wThis is a creepy lab where horrible experiments occur (# of monsters = 1) (# of energy bowls = 5) (# of keys = 1) (# of locks = 26)A robot crushes Buzz (damage = 8)Buzz zaps a robot (damage = 1)done-> checkBuzz is a futuristic spaceman with a lazer gun (Buzz.energy = 91) (Buzz.keys = 0)done
The Design
Players will use a game console to navigate their heroes through a maze of monster-
inhabited rooms. A game factory decouples the construction of the maze from the
construction of monsters and rooms. Here's our design:
3-27
Pattern Oriented Programming with C++/Pearce
Customizations of FUN will be expected to complete implementations of critical functions
in classes derived from the abstract Hero, Room, and Monster classes.
The Cast of Characters
Although we don't know the exact nature of heroes and monsters, we can introduce abstract
Hero and Monster base classes in FUN. These classes derive from FUN's Character base
class:
3-28
3. Frameworks, Kits, and Polymorphism
class Character{public:
Character(string nm = "unknown"){
name = nm;energy = 100;
}virtual ~Character() {}int getEnergy() { return energy; }void setEnergy(int e) { energy = e; }string getName() { return name; }int injure(Character* c);virtual void describe() = 0;
protected:int energy; // dead = 0% <= energy <= 100%string name;
};
Characters can cause injuries to other characters using the injure() member function.
Injuring a character randomly lowers that character's energy by an amount between 0 and
MAX, where:
MAX = DAMAGE_CONTROL + energy/c->energy
DAMAGE_CONTROL is a constant that can be redefined by FUN customizations. Notice
that the maximum amount of damage also depends on the comparative strengths of the
attacker and the victim. Injuring a character also causes a small depletion in the attacker's
energy level. Of course exceptions are thrown if either attacker or victim is initially dead:
int Character::injure(Character* c){
if (!energy) throw AppError(name + DEAD);if (!c->energy) throw AppError(c->name + DEAD);int damageCaused = rand() % (DAMAGE_CONTROL + energy/c->energy);int damageSustained = 1; // cuz fight'n is tough workc->energy = max(c->energy - damageCaused, 0);energy = max(energy - damageSustained, 0);return damageCaused;
}
The only thing we can say in FUN about monsters is that they are characters who attack
heroes:
3-29
Pattern Oriented Programming with C++/Pearce
class Monster: public Character{public:
Monster(string nm = "unknown"): Character(nm) {}virtual ~Monster() {}virtual void attack(Hero* h) = 0;virtual void describe() = 0;
};
We can say more about heroes in FUN. A hero is a character who defends himself against
monsters. In addition, a hero can move from room to room, collect keys, pick fights with
monsters, and invigorate himself by drinking from energy bowls:
class Hero: public Character{public:
Hero(string nm = "unknown"): Character(nm) { keys = 0; room = 0; }virtual ~Hero() {}virtual void defend(Monster* m) = 0;void move(Direction d); // move to a new roomvoid invigorate(); // drink room's energy bowlsint getKeys() { return keys; }void takeKeys(); // collect room's keysRoom* getRoom() { return room; }void setRoom(Room* r) { room = r; }void fight(); // fight room's monstersvirtual void describe() = 0;
private:Room* room; // = current locationint keys; // = # of keys collected
};
Even though describe() is a pure virtual function in both the Character and Hero classes, we
can still provide them with implementations:
void Character::describe(){
cout << " (" << name << ".energy = " << energy << ")\n";}
void Hero::describe(){
Character::describe();cout << " (" << name << ".keys = " << keys << ')' << endl;
}
Declaring a member function to be pure virtual requires concrete derived classes to
overwrite it. However, the overwrite can call the partially implemented function inherited
from the abstract base class.
3-30
3. Frameworks, Kits, and Polymorphism
The Field of Battle
What can we say in FUN about rooms? We can't say what rooms look like, but we can say
that each room has keys, locks, energy bowls, monsters, and up to four neighboring rooms.
class Room{public:
Room(int e = 0, int k = 0, int l = 500);virtual ~Room() {}virtual void enter(Hero* h);int takeEnergy(Hero* h); // give energy bowls to hint takeKey(Hero* h); // give keys to hvirtual void describe() = 0; // display or printvirtual void attack(Hero* h); // monsters attack hint getLocks() { return locks; }void setEnergy(int bowls) { energy = bowls; }void setKeys(int k) { keys = k; }void setLocks(int l) { locks = l; }void add(Monster* m) { monsters.push_back(m); }Room* getNeighbor(Direction d) { return neighbors[int(d)]; }void setNeighbor(Direction d, Room* r) {
neighbors[int(d)] = r;}
private:int keys; // = # of keys to be collected (< locks)int energy; // = # of energy bowls vector<Monster*> monsters;Room* neighbors[4];bool visited; // = true after 1st hero visit (not used)int locks; // = # of keys needed to escape this room
};
When a hero enters a room, he immediately escapes if the number of keys he has collected
is greater than or equal to the number of locks in the room. Otherwise, the room is
described by calling the virtual describe() function, which must be defined by FUN
customizations. Next, the monsters attack:
void Room::enter(Hero* h){
h->setRoom(this);if (h->getKeys() >= locks) // hero wins game!
throw AppError(h->getName() + ESCAPED); describe(); // describe the room to hattack(h); // get h!visited = true; // h waz here (may or may not be important)
}
3-31
Pattern Oriented Programming with C++/Pearce
The attack() function uses the Generic Algorithm pattern. Each live monster in the room
attacks the hero, and the hero defends himself against the monster. Of course the actual
meaning of attack and defend must be determined by customizations. If the hero is dead,
either before or after the attack, an exception is thrown that ends the game. After the
fighting, the dead monsters are removed from the room:
void Room::attack(Hero* h){
for(int i = 0; i < int(monsters.size()); i++)if (monsters[i]->getEnergy()){
if (!h->getEnergy()) throw AppError(h->getName() + DEAD);monsters[i]->attack(h);if (!h->getEnergy())
throw AppError(h->getName() + " has been killed!");h->defend(monsters[i]);if (!monsters[i]->getEnergy())
cout << "A monster has been killed!\n";}
// clean out the corpses:vector<Monster*>::iterator p = monsters.begin();for( ; p != monsters.end(); ){
if (!(*p)->getEnergy()) monsters.erase(p);// p++ if previous line didn't advance p to the end:if (p != monsters.end()) p++;
}}
The Maze uses the Singleton pattern to insure that there is at most one maze. The maze
contains a two dimensional array of Room pointers. The dimensions of the array are
constants that can be redefined in the customization:
3-32
3. Frameworks, Kits, and Polymorphism
class Maze{public:
static Maze* makeMaze(GameFactory* gf = 0){
if (!theMaze) theMaze = new Maze(gf);return theMaze;
}void add(Room* room, int i, int j);Room* getRoom(int i, int j){
if (i < 0 || ROWS <= i || j < 0 || COLS <= j)throw AppError("room index out of range");
return rooms[i][j];}void describe(); // describe every room in this maze
private:static Maze* theMaze;Maze(GameFactory* gf = 0);Maze(const Maze& m) {}~Maze() {}Room* rooms[ROWS][COLS];
};
The private Maze constructor is parameterized by a toolkit called a game factory, which is
used to create rooms and monsters. By default, each room in the maze contains one key,
five energy bowls, and a number of locks equal to the total number of keys plus one. Only
the room in the North-West corner has a number of locks equal to the total number of keys.
Thus, the hero will have to visit every room in the maze, collect all of the keys, then return
to the North-West corner room to escape. Also, the number of monsters per room increases
as the hero moves toward the South-East corner.
Maze::Maze(GameFactory* gf){
srand(time(0)); // seed rand()// pre-initialize room pointers:for(int i = 0; i < ROWS; i++)
for(int j = 0; j < COLS; j++)rooms[i][j] = 0;
// create rooms, keys, locks, bowls, & monsters:for(int i = 0; i < ROWS; i++)
for(int j = 0; j < COLS; j++){
add(gf->makeRoom(), i, j);// add i + j monsters to this room:for(int k = 0; k < numMonsters; k++)
rooms[i][j]->add(gf->makeMonster());}
rooms[0][0]->setLocks(KEYS); // the only way out}
3-33
Pattern Oriented Programming with C++/Pearce
Of course the number of keys, locks, monsters, energy bowls, and neighbors a room has
can be easily adjusted in framework customizations.
The FUN framework provides an abstract factory for making game components:
class GameFactory{public:
virtual ~GameFactory() {}virtual Monster* makeMonster() = 0;virtual Hero* makeHero() = 0;virtual Room* makeRoom(int e = 5, int k = 1, int l = KEYS + 1) = 0;virtual Maze* makeMaze() { return Maze::makeMaze(this); }
};
Dungeons and Dragons
Dungeons and Dragons is an adventure game set in Medieval times. We implement
Dungeons and Dragons as a customization of the FUN framework. Monsters are fire-
breathing dragons:
class Dragon: public Monster{public:
Dragon(string nm = "dragon"): Monster(nm) {}void attack(Hero* h){
cout << "A dragon flames " << h->getName();cout << " (damage = " << injure(h) << ')' << endl;
}void describe(){
cout << "Here is a fire-breathing dragon\n";Monster::describe();
}};
Heroes are brave knights who defend themselves from dragons by slashing at them with
sharp swords:
3-34
3. Frameworks, Kits, and Polymorphism
class Knight: public Hero{public:
Knight(string nm = "Lancelot"): Hero(nm) {}void defend(Monster* m){
cout << name << " slashes at a dragon";cout << " (damage = " << injure(m) << ")\n";
}void describe(){
cout << name << " is a brave knight with a sharp sword\n";Hero::describe();
}};
And rooms are dark, damp dungeons:
class Dungeon: public Room{public:
Dungeon(int e = 0, int k = 0, int l = 500): Room(e, k, l) {}void describe(){
cout << "This is a dark, damp dungeon where prisoners rot\n";Room::describe();
}};
Dungeons and Dragons implements the factory methods of FUN's GameFactory so that
they produce Dragons, Knights, and Dungeons:
class DNDFactory: public GameFactory{public:
Monster* makeMonster() { return new Dragon(); }Hero* makeHero() { return new Knight(); }Room* makeRoom(int e = 5, int k = 1, int l = KEYS + 1){
return new Dungeon(e, k, l);}
};
Starting a game is done by customizations. For example, here is how Dungeons and
Dragons gets started:
int main(){
GameConsole console(new DNDFactory());console.controlLoop();return 0;
}
3-35
Pattern Oriented Programming with C++/Pearce
Pipelines
A pipe is a message queue. A message can be anything. A filter is a process, thread, or
other component that perpetually reads messages from an input pipe, one at a time,
processes each message, then writes the result to an output pipe. Thus, it is possible to form
pipelines of filters connected by pipes:
The inspiration for pipeline architectures probably comes from signal processing. In this
context a pipe is a communication channel carrying a signal (message), and filters are
signal processing components such as amplifiers, noise filters, receivers, and transmitters.
Pipelines architectures appear in many software contexts. (They appear in hardware
contexts, too. For example, many processors use pipeline architectures.) UNIX and DOS
command shell users create pipelines by connecting the standard output of one program
(i.e., cout) to the standard input of another (i.e., cin):
% cat inFile | grep pattern | sort > outFile
In this case pipes (i.e., "|") are inter process communication channels provided by the
operating system, and filters are any programs that read messages from standard input, and
write their results to standard output.
LISP programmers can represent pipes by lists and filters by list processing procedures.
Pipelines are built using procedural composition. For example, assume the following LISP
procedures are defined4. In each case the nums parameter represents a list of integers:
// = list got by removing even numbers from nums(define (filterEvens nums) ... )
// = list got by squaring each n in nums(define (mapSquare nums) ... )
// = sum of all n in nums(define (sum nums) ... )
4 See Programming Note 8 in Appendix 1..
3-36
3. Frameworks, Kits, and Polymorphism
We can use these procedures to build a pipeline that sums the squares of odd integers:
Here's the corresponding LISP definition:
// = sum of squares of odd n in nums(define (sumOddSquares nums)
(sum (mapSquare (filterEvens nums))))
Pipelines have also been used to implement compilers. Each stage of compilation is a filter:
The scanner reads a stream of characters from a source code file and produces a stream of
tokens. A parser reads a stream of tokens and produces a stream of parse trees. A translator
reads a stream of parse trees and produces a stream of assembly language instructions. We
can insert new filters into the pipeline such as optimizers and type checkers, or we can
replace existing filters with improved versions.
There's even a pipeline design pattern:
Pipes and Filters [POSA]
Other Names
Pipelines
Problem
The steps of a system that processes streams of data must be reusable, re orderable, replaceable, and/or independently developed.
Solution
Implement the system as a pipeline. Steps are implemented as objects called filters. Filters receive inputs from, and write outputs to streams called pipes. A filter knows the identity of its input and output pipes, but not its neighboring filters.
Filter Classification
There are four types of filters: producers, consumers, transformers, and testers. A
producer is a producer of messages. It has no input pipe. It generates a message into its
3-37
Pattern Oriented Programming with C++/Pearce
output pipe. A consumer is a consumer of messages. It has no output pipe. It eats messages
taken from its input pipe. A transformer reads a message from its input pipe, modulates it,
then writes the result to its output pipe. (This is what DOS and UNIX programmers call
filters.) A tester reads a message from its input pipe, then tests it. If the message passes the
test, it is written, unaltered, to the output pipe; otherwise, it is discarded. (This is what
signal processing engineers call filters).
Filters can also be classified as active or passive. An active filter has a control loop that
runs in its own process or thread. It perpetually reads messages from its input pipe,
processes them, then writes the results to its output pipe. An active filter needs to be
derived from a thread class provided by the operating system:
class Filter: public Thread { ... };
An active filter has a control loop function. Here's a simplified version that assumes the
filter is a transformer:
void Filter::controlLoop(){
while(true){
Message val = inPipe->read();val = transform(val); // do something to valoutPipe->write(val);
}}
We will explore active filters in Chapter 7.
When activated, a passive filter reads a single message from its input pipe, processes it,
then writes the result to its output pipe:
void Filter::activate(){
Message val = inPipe->read();val = transform(val); // do something to valoutPipe->write(val);
}
There are two types of passive filters. A data-driven filter is activated when another filter
writes a message into its input pipe. A demand-driven filter is activated when another
filter attempts to read a message from its empty output pipe.
3-38
3. Frameworks, Kits, and Polymorphism
Dynamic Structure: Data-Driven
Assume a particular data-driven pipeline consists of a producer connected to a transformer,
connected to a consumer. The producer writes a message to pipe 1, the transformer reads
the message, transforms it, then writes it to pipe 2. The consumer reads the message, then
consumes it:
Dynamic Structure: Demand-Driven
A data-driven pipeline pushes messages through the pipeline. A demand-driven pipeline
pulls messages through the pipeline. Imagine the same set up using demand-driven passive
filters. This time read operations propagate from the consumer back to the producer. A
message is produced and written to pipe 1. The transformer reads the message, transforms
it, then writes it to pipe 2. This message is the value returned by the consumer's original
call to read():
3-39
Pattern Oriented Programming with C++/Pearce
A Problem
Both diagrams reveal a design problem. How does the transformer know when to call
pipe1.read()? How does the data-driven consumer know when to call pipe2.read()? How
does the demand-driven producer know when to produce a message? Active filters solve
this problem by polling their input pipes or blocking when they read from an empty input
pipe, but this is only feasible if each filter is running in its own thread or process.
We could have the producer in the data-driven model signal the transformer after it writes a
message into pipe 1. The transformer could then signal the consumer after it writes a
message into pipe 2. In the demand-driven model the consumer could signal the
transformer when it needs data, and the transformer could signal the producer when it
needs data. But this solution creates dependencies between neighboring filters. The same
transformer couldn't be used in a different pipeline with different neighbors.
Our design problem fits the same pattern as the problem of how the reactor in Chapter 2
communicates with its unknown monitors. We solved that problem by making the reactor a
publisher and the monitors subscribers. We can use the publisher-subscriber pattern here,
too. Pipes are publishers and filters are subscribers. In the data-driven model filters
subscribe to their input pipes. In the demand-driven model filters subscribe to their our
output pipes.
3-40
3. Frameworks, Kits, and Polymorphism
Pipes: A Pipeline Toolkit
How can we provide a toolkit for building data-driven pipelines? The toolkit should
provide factory methods for making different types of filters: testers, transformers,
producers, and consumers. Each factory method should be parameterized by the input and
output pipes, if needed, and a pointer to a function to be used for testing, transforming,
producing, or consuming a single message:
Filter* PipelineKit<Msg>::makeProducer(ProducerProc f, Pipe* op);Filter* PipelineKit<Msg>::makeTransformer(
Pipe* ip, TransformerProc f, Pipe* op);Filter* PipelineKit<Msg>::makeTester(
Pipe* ip, TesterProc f, Pipe* op);Filter* PipelineKit<Msg>::makeConsumer(Pipe* ip, ConsumerProc f);
Of course the kit should allow users to build pipelines that process any type of message. To
accomplish this, the kit can make no assumptions about message types. In C++ we can
make kit a template parameterized by message type:
template <class Msg> class PipelineKit { ... };
For example, our test driver creates a pipeline that computes the sum of even squares. We
begin by defining several simple global functions:
int square(int x) { return x * x; }
bool isEven(int x) { return x % 2 == 0; }
void accum(int x){
static int total = 0; // retains its value between callstotal += x;cout << total << endl;
}
bool getNumber(int& val){
cin >> val;return cin; // = false if failure
}
The main() function uses the pipeline kit to create pipeline:
3-41
Pattern Oriented Programming with C++/Pearce
int main(){
typedef PipelineKit<int> Kit; // for brevityKit::PipePtr p1, p2, p3;Kit::FilterPtr f1, f2, f3, f4;
p1 = Kit::makePipe();p2 = Kit::makePipe();p3 = Kit::makePipe();
// f1-p1->f2-p2->f3-p3->f4f1 = Kit::makeProducer(getNumber, p1);f2 = Kit::makeTester(p1, isEven, p2);f3 = Kit::makeTransformer(p2, square, p3);f4 = Kit::makeConsumer(p3, accum);
f1->start();return 0;
}
To run the program, we need to create several files of test numbers. For example, the file
nums.txt contains the following entries:
1234 5 6789101112
We can use file redirection to connect the standard input, cin in getNumber(), to nums.txt,
and the standard output, cout in accum(), to a new file called squares:
D:\> soes < nums.txt > squares
Here's the content of squares after the run:
42056120220364
We can also run our program interactively. The boldface text indicates program output:
3-42
3. Frameworks, Kits, and Polymorphism
C:/> soes1243420565678120910220quitC:/>
From the test driver we can infer that the kit's factory methods are class functions (i.e. static
functions) that return pointers to pipes and filters. We can also see that pipes and filters are
inner classes, more on this later.
Design
Users of a pipeline toolkit will want to add and remove filters from pipelines without too
much difficulty. Therefore, filters in a pipeline should be independent of each other. In
other words, a filter should only know the identity of the pipes that connect it to its
neighboring filters, not the neighbors themselves. A pipe should be able to connect any
filters. Therefore, although a filter may know the identity of its input and output pipes, a
pipe is loosely coupled to the filters it connects. As mentioned earlier, this creates a
notification problem. How will a pipe in a data-driven pipeline notify its downstream filter
that a message has arrived? We use the Publisher-Subscriber problem to solve this
problem. Pipes are publishers, and filters are subscribers that subscribe to their input pipes
in data-drive pipelines.
Using the Publisher-Subscriber pattern may be an example of over-design. After all, a pipe
only has one filter it must notify when a message arrives. Although we don't demonstrate
this feature, our pipes can also be used as tees. A tee can connect a filter to multiple
downstream filters.
3-43
Pattern Oriented Programming with C++/Pearce
In this case a pipe may need to notify unknown numbers and types of downstream filters
when a message arrives.
Although a particular pipeline processes a particular type of message, different pipelines
may process different types of messages. In any case, the toolkit can make no assumption
about the types of messages that will be processed. This information will be supplied by
users as they build pipelines. We could deal with this missing information the same way
the FUN framework dealt with monsters and heroes: by introducing Msg (Message) as an
abstract base class. For variety, PIPES takes a different approach: Msg will be a template
parameter.
The following class diagram shows the relationships between the principle classes of the
PIPES framework. The toolkit itself is not shown:
The Filter base class maintains one or two pointers to pipes. These are the input and output
pipes. It also provides a virtual start function that is redefined in the Producer class. This is
3-44
3. Frameworks, Kits, and Polymorphism
the engine that drives the pipeline. In a demand-driven pipeline the consumer implements
start(). Filter is shown as an abstract class because it doesn't provide an implementation of
the pure virtual update() function inherited from the Subscriber class. This job is left to the
Filter-derived classes.
Implementation
The entire toolkit is contained in a header file called pipes.h. This file contains the
declaration of a class of exceptions programmers must throw if minor problems arise:
class PipeLineError: public runtime_error{public:
PipeLineError(string gripe = "unknown"): runtime_error(gripe){}
};
In addition, the file contains the pipeline kit template:
template <class Msg>class PipelineKit{
class Pipe: public Publisher { ... };class Filter: public Subscriber { ... };class Transformer: public Filter { ... };class Producer: public Filter { ... };class Consumer: public Filter { ... };class Tester: public Filter { ... };// etc.
};
We have made Pipe, Filter, Producer, etc. inner classes of PipelineKit. This is common for
toolkits, as it offers the possibility of coexisting pipeline toolkits that create different types
of pipes and filters. Each toolkit hides its own pipe and filter implementations.
Using Function Pointers
Reexamine how the test driver created its four filters:
f1 = Kit::makeProducer(getNumber, p1);f2 = Kit::makeTester(p1, isEven, p2);f3 = Kit::makeTransformer(p2, square, p3);f4 = Kit::makeConsumer(p3, accum);
3-45
Pattern Oriented Programming with C++/Pearce
In each case the name of the message processing function was passed as an argument. This
seems a bit strange, at first. Normally, argument lists follow function names:
f(a, b, c);
A function name that isn't followed by an argument list is similar to an array name that isn't
followed by an index. For example, if nums is an array, then nums[0] is the same as *nums,
but by itself, nums is a pointer to the beginning of the array. Similarly, sin(0) calls the sin
function, but by itself, sin is just a pointer to the first binary instruction of the sin function.
Like any pointer, it can be passed as a parameter, assigned to a variable, or returned as a
value.
How would we declare a variable, f, that can hold a pointer to a function like sin? The
syntax is a little tricky:
double (*f)(double);
This declares f to be a pointer to a function that expects a double argument and returns a
double value. The assignment:
f = sin;
points f at the same place sin points to, namely the first instruction of the sin function. After
the assignment, we can call f the same way we call sin:
cout << f(0) << endl; // prints sin(0)
Later, we can reassign another function to f:
f = cos;
Now calling f is the same as calling cos:
cout << f(0) << endl; // prints cos(0)
In a sense, treating functions like data adds another degree of flexibility to our programs
beyond polymorphism. We can place a call to f in a program and each time this line is
executed, a different function might be called.
Writing typedef in front of a function pointer declaration:
3-46
3. Frameworks, Kits, and Polymorphism
typedef double (*f)(double);
declares f not to be the name of a pointer to a function, but rather a name for the type of all
pointers to functions. Let's change the name of the type to something a bit grander:
typedef double (*Fun)(double);
We can now declare and assign f using the syntax:
Fun f = sin;
If we reexamine the factory method parameter lists, we see that names have been
introduced for the various types of message processing functions:
makeProducer(ProducerProc f, Pipe* op);makeTransformer(Pipe* ip, TransformerProc f, Pipe* op);makeTester(Pipe* ip, TesterProc f, Pipe* op);makeConsumer(Pipe* ip, ConsumerProc f);
These names are introduced by typedefs that occur at the beginning of the PipelineKit
declaration:
template <class Msg>class PipelineKit{
// function pointer types:typedef Msg (*TransformerProc)(Msg);typedef bool (*TesterProc)(Msg);typedef void (*ConsumerProc)(Msg);typedef bool (*ProducerProc)(Msg&);// etc.
};
Take a moment to review these types. A TransformerProc expects a message as input and
returns a transformed message as output. A TesterProc expects a message as input and
returns true if the message passes the test, and false, otherwise. A ConsumerProc expects a
message as input, and returns nothing, because it has consumed the message. A
ProducerProc expects a reference to a message variable as input. This is a place where it
can store the message it produces. If successful, it returns true, otherwise it returns false.
3-47
Pattern Oriented Programming with C++/Pearce
Factory Methods
The PipelineKit template begins with private typedefs and inner class declarations. The
actual bodies of the inner classes appear separately to make things more readable. The
public part of the kit includes static factory methods for creating pipes and filters:
template <class Msg>class PipelineKit{
// inner typespublic:
typedef Pipe *PipePtr;typedef Filter *FilterPtr;// factory methods, ip = input pipe, op = output pipestatic FilterPtr makeProducer(ProducerProc f, Pipe* op){
return new Producer(f, op);}static FilterPtr makeTransformer(Pipe* ip,
TransformerProc f, Pipe* op){
return new Transformer(ip, f, op);}static FilterPtr makeTester(Pipe* ip, TesterProc f, Pipe* op){
return new Tester(ip, f, op);}static FilterPtr makeConsumer(Pipe* ip, ConsumerProc f){
return new Consumer(ip, f);}PipePtr makePipe() { return new Pipe(); }
};
Pipes
Initially, we defined a pipe as a message queue. This is only necessary for pipelines that
contain active filters, where one filter may write several messages to its out pipe before the
downstream filter has finished processing its current message. Because our filters are
passive, and because our pipes will notify their readers each time a message is written to
them, it won't be possible for several messages to be written before a single message is
read. Therefore, a pipe only needs to store a single message, which is replaced each time
write() is called:
3-48
3. Frameworks, Kits, and Polymorphism
class Pipe: public Publisher{public:
Msg read() { return message; }void write(Msg val){
message = val;notify(); // data driven
}private:
Msg message; // the stored message};
Filters
The filter base class maintains pointers to its input and output pipes. Declaring them to be
protected makes them visible to the Filter-derived classes. For producers, the input pipe
will be 0, for consumers, the output pipe will be 0. The start() function will be redefined in
the Producer class:
class Filter: public Subscriber{public:
Filter(Pipe *ip = 0, Pipe* op = 0){
inPipe = ip;outPipe = op;if (inPipe) inPipe->Subscribe(this);
}virtual ~Filter(){
if (inPipe) inPipe->unsubscribe(this);}virtual void start() // redefine in Producer{
throw PipeLineError("This is not a producer.\n");}
protected:Pipe *inPipe, *outPipe;
};
The destructor unsubscribes a data-driven filter from its input pipe. Subscribing to the input
pipe will be done in the derived classes that have input pipes.
Notice that Pipe does not implement the pure virtual update() function inherited from
Subscriber. This makes Filter an abstract class. We will not be able to create any generic
filters. Instead, update() must be implemented by classes derived from filter.
3-49
Pattern Oriented Programming with C++/Pearce
Transformers
A transformer implements update() by reading a message from its inherited input pipe,
transforming the message using the function pointer passed to its constructor, then writing
the transformed message to its inherited output pipe:
class Transformer: public Filter{public:
Transformer(Pipe* ip, TransformerProc f, Pipe* op): Filter(ip, op){
transform = f;}void update(Publisher* p, void* what){
Msg val = inPipe->read();val = transform(val);outPipe->write(val);
}private:
TransformerProc transform;}; // Transformer
Testers
A tester implements update() by reading a message from its inherited input pipe, then
writing the message to its inherited output pipe if it passes a test given by the function
pointer passed to the constructor:
class Tester: public Filter{public:
Tester(Pipe* ip, TesterProc f, Pipe* op): Filter(ip, op){
test = f;}void update(Publisher* p, void* what){
Msg val = inPipe->read();if (test(val)) outPipe->write(val);
}private:
TesterProc test;}; // Tester
3-50
3. Frameworks, Kits, and Polymorphism
Consumers
A consumer implements update() by reading a message from its inherited input pipe, then
passing the message to a consumer function pointer specified by the constructor:
class Consumer: public Filter{public:
Consumer(Pipe* ip, ConsumerProc f): Filter(ip, 0){
consume = f;}void update(Publisher* p, void* what){
Msg val = inPipe->read();consume(val);
}private:
ConsumerProc consume;}; // Consumer
Producers
A data-driven producer implements update() as a no-op. This is because the producer has
no input pipe that is subscribes to. A producer must redefine the inherited start() function.
The start() function is the engine that drives a data-driven pipeline. It repeatedly calls the
producer function initialized by the constructor. By assumption, this function either stores a
new message in the val variable and returns true (recall that the producer's parameter is a
reference), or it returns false. If it returns true, the message in val is written to the inherited
output pipe and the loop repeats. Otherwise the start() function terminates. This terminates
the flow of messages through the pipeline. If an exception is thrown anywhere in the
pipeline, it will be caught by the producer:
3-51
Pattern Oriented Programming with C++/Pearce
class Producer: public Filter{public:
Producer(ProducerProc f, Pipe* op): Filter(0, op){
produce = f;}void update(Publisher* p, void* what) {} // no opvoid start(){
bool more = true; Msg val;while(more)
try{
more = produce(val);if (more) outPipe->write(val);
}catch(PipeLineError e){
cerr << e.what() << endl;}catch(...){
cerr << "Unknown error! Shutting pipeline down\n";more = false;
}}
private:ProducerProc produce;
};
Programming Notes
Programming Note 3.1: Implementing FUN
The FUN adventure game framework consists of the following files and classes:
cast.h & cast.cpp (Character, Hero, Monster)place.h & place.cpp (Room, Maze)ui.h & ui.cpp (GameFactory, GameConsole)defs.h (Direction & constants)
defs.h
Constants that control the play of the game are stored in defs.h, which is included by most
of the other header files in FUN:
3-52
3. Frameworks, Kits, and Polymorphism
/* * File: pop\chapter3\defs.h * Programmer: Pearce * Copyright (c): 2000, all rights reserved. */ #ifndef DEFS_H#define DEFS_H
#include "..\util\console.h" // for ui#include <cmath> // for rand() & srand()#include <time.h> // for time()
// maze directions:enum Direction {N, E, S, W};// maze size:#define ROWS 5#define COLS 5#define KEYS ROWS * COLS // assumes 1 key/room// some standard error message suffixes:#define DEAD " is dead!"#define ESCAPED " has escaped!"#define LOST " is lost!"// minimum maximum damge inflicted:#define DAMAGE_CONTROL 10#define ENERGY 5 // energy bowls/room#define LOCKS 5 // minimum # of locks/room// collect keys only when # monsters <#define MIN_MONSTERS 2
#endif
ui.h
In addition to the abstract GameFactory class defined earlier, ui.h declares the game
console class, which is derived from the Console class. (The Console class was described
in Programming Note 2.2 and is one of our utility classes.) In this case, the game console's
context is the maze and a hero:
3-53
Pattern Oriented Programming with C++/Pearce
class GameConsole: public Console{public:
GameConsole(GameFactory* gf = 0) {
myHero = gf? gf->makeHero(): 0;myMaze = gf? gf->makeMaze(): 0;myHero->setRoom(myMaze->getRoom(0, 0));
}private:
string execute(const string& cmmd);void help();Hero* myHero;Maze* myMaze;
};
ui.cpp
As required by the Console base class, we must provide an implementation for the
execute() function:
string GameConsole::execute(const string& cmmd){
if (cmmd == "move") {
string dir;cin >> dir;if (dir == "N" || dir == "n") myHero->move(N);else if (dir == "S" || dir == "s") myHero->move(S);else if (dir == "E" || dir == "e") myHero->move(E);else if (dir == "W" || dir == "w") myHero->move(W);else throw AppError("Illegal direction");
}else if (cmmd == "drink") myHero->invigorate();else if (cmmd == "scan") myHero->getRoom()->describe();else if (cmmd == "check") myHero->describe();else if (cmmd == "fight") myHero->fight();else if (cmmd == "grab") myHero->takeKeys();else if (cmmd == "map") myMaze->describe();else if (cmmd == "super") myHero->setEnergy(10000);else throw AppError(unrecognized + cmmd);return "done";
}
We can also redefine the help() function to display the command menu.
place.cpp
The definition and initialization of the static member variable from the Maze class is placed
near the top of place.cpp:
3-54
3. Frameworks, Kits, and Polymorphism
Maze* Maze::theMaze = 0;
The rest of the file contains implementations of the Maze and Room member functions. For
example, here's the function that adds rooms to the maze:
void Maze::add(Room* room, int i, int j){
if (i < 0 || ROWS <= i || j < 0 || COLS <= j)throw AppError("room index out of range");
rooms[i][j] = room;if (0 < j && rooms[i][j - 1]) {
room->setNeighbor(W, rooms[i][j - 1]);rooms[i][j - 1]->setNeighbor(E, room);
}if (j + 1 < COLS && rooms[i][j + 1]) {
room->setNeighbor(E, rooms[i][j + 1]);rooms[i][j + 1]->setNeighbor(W, room);
}if (0 < i && rooms[i - 1][j]){
room->setNeighbor(N, rooms[i - 1][j]);rooms[i - 1][j]->setNeighbor(S, room);
}if (i + 1 < ROWS && rooms[i + 1][j]){
room->setNeighbor(S, rooms[i + 1][j]);rooms[i + 1][j]->setNeighbor(N, room);
}}
character.cpp
Here is how the hero moves to another room:
void Hero::move(Direction d){
if (!energy) throw AppError(name + DEAD);if (!room) throw AppError(name + LOST);if (!room->getNeighbor(d)) throw AppError("That door is locked");room = room->getNeighbor(d);room->enter(this);
}
The invigorate() function is called when the hero drinks energy bowls:
3-55
Pattern Oriented Programming with C++/Pearce
void Hero::invigorate(){
if (!energy) throw AppError(name + DEAD);if (!room) throw AppError(name + LOST);energy = min(energy + room->takeEnergy(this), 100);
}
If the hero wants to, he can pick a fight with the monsters in his current room:
void Hero::fight(){
if (!energy) throw AppError(name + DEAD);if (!room) throw AppError(name + LOST);room->attack(this);
}
Other character.cpp functions are relatively straight forward and are left to the reader.
Problems
Problem 3.1
Create and test a template version of the UIToolKit.
Problem 3.2
Complete the FUN framework. (A few function definitions are missing.) Test FUN by
using it to create and play an adventure game set in outer space.
3.1 Problem: A Two Player Game Framework
Most two player card and board games follow the same basic pattern:
Player 1 and player 2 alternate turns. Usually, points are scored on each turn. When one player accumulates enough points, he wins and the game is over.
We can capture this pattern in a generic algorithm, which can be the basis for a mini
framework for developing two player computer games. The framework allows tournaments
consisting of multiple games, so it keeps track of the total number of player 1 and player 2
wins, which it displays with the showStats() function. The generic algorithm is play()5:
5 We follow the MacApp convention of prefixing functions that must be defined in the derived class by "do".
3-56
3. Frameworks, Kits, and Polymorphism
void Game::play(){
bool again = true;
while (again){
score1 = score2 = 0;doInitGame(); // reinitialize game environment
while(!doGameOver())try{
cout << "\nPlayer 1's turn ...\n";score1 += doPlayer1();if (!doGameOver()){
cout << "\nPlayer 2's turn ...\n";score2 += doPlayer2();
}}catch(runtime_error e){
cerr << "error: " << e.what() << endl;}
switch (doWinner()) // determine the winner{case 1:
cout << "\nPlayer 1 wins!\n";player1wins++;break;
case 2:cout << "\nPlayer 2 wins!\n";player2wins++;break;
default:cout << "\ndraw\n";
}
showStats(); // print totalsagain = getResponse("Again?"); // declared in utils.cpp
} // while again} // play()
The generic algorithm's while loop allows tournament play. After each game, the total
number of player 1 and player 2 wins is displayed, and the user is prompted for another
game. Actual play of the game is risky, so doPlayer1() and doPlayer2() are called inside of
a try-catch block.
3-57
Pattern Oriented Programming with C++/Pearce
3.1.1.: Tic Tac Toe
Although player 1 and player 2 could both be humans, player 2 is more commonly the
computer. Customize the framework to allow users to play Tic Tac Toe with the computer.
The computer will be player 2. Here's the output produced near the end of one game, player
1 is X, and player 2 is O:
Player 1's turn ...O _ O_ X _X _ _Pick a row -> 2You entered 2Pick a column -> 2You entered 2
Player 2's turn ...O O O_ X _X _ X
Player 2 wins!# of player 1 wins = 0# of player 2 wins = 1Again? (y/n) ->
3.1.2. Problem: Blackjack
Customize the game framework a different way to create a program that allows users to
play Blackjack with the computer. You may not alter the framework.
A game of Blackjack consists of dealing a hand of two random cards to the user, then
printing the value of the hand. The value is the sum of the card values, where:
ACE = 1 or 11TWO = 2etc.NINE = 9TEN = JACK = QUEEN = KING = 10
The user is repeatedly asked if he wants another card until he says no (holds) or until the
value of the hand exceeds 21, in which case the game is over and the user looses.
If the user holds, then the computer deals two random cards to itself and displays the sum.
The computer continues to deal itself a card until the sum of the cards is greater than or
3-58
3. Frameworks, Kits, and Polymorphism
equal to 17. If the sum is greater than 21 or less than the sum of the user's hand, then the
computer looses, if the sums are the same, then the game ends in a draw. Otherwise, the
computer wins.
Hints
Represent a card as a structure consisting of three variables:
struct Card{
Value value;Suit suit;bool dealt; // = true if played
};
Represent suits and values as enumerations:
enum Suit {CLUB, DIAMOND, HEART, SPADE};
enum Value {
ACE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING
};
And represent decks as arrays of 52 cards:
typedef Card Deck[52];
Here's a tricky way to initialize a deck:
void init(Deck d){
for(int i = 0; i < 4; i++)for(int j = 0; j < 13; j++){
int n = 13 * i + j; // 0 <= n < 52d[n].suit = Suit(i); // why is explicit cast needed?d[n].value = Value(j); d[n].dealt = false;
}}
Shuffling a deck is just a matter of clearing the dealt flags:
3-59
Pattern Oriented Programming with C++/Pearce
void shuffle(Deck d) // why is value param okay here?{
for(int i = 0; i < 52; i++)d[i].dealt = false;
}
To pick a card, generate a random number between 0 and 51. If the card at that position is
dealt, then increment the number modulo 52 until an undealt card is found:
Card pickCard(Deck d) {
int i = rand() % 52; // don't forget to seed using srand()int j = i;while (d[j].dealt){
j = (j + 1) % 52;if (i == j)
throw runtime_error("No more cards");}d[j].dealt = true;return d[j];
}
3.2. Problem: Using Factory Method Templates
The adventure game framework can also use factory method templates. Assume M, H, and
R are template parameters representing the Monster, Hero, and Robot constructors,
respectively. We can implement most of the factory methods in the game factory template:
template <typename M, typename H, typename R> class Maze; // forward reference
template <typename M, typename H, typename R>class GameFactory{public:
Monster* makeMonster(string nm = "???") { return new M(nm); }Hero* makeHero(string nm = "???") { return new H(nm); }Room* makeRoom() { return new R(); }virtual Maze<M, H, R>* makeMaze() = 0;
};
Finish this implementation of the adventure game framework. Customize the framework to
create two adventure games different from the games created in my example.
3.3. Problem: C++ Review
Is the declaration:
3-60
3. Frameworks, Kits, and Polymorphism
Shape *s1 = new Circle(), *s2 = new Rectangle(), *s3 = new Triangle();
equivalent to the following declaration:
Shape* s1 = new Circle(), s2 = new Rectangle(), s3 = new Triangle();
3.4. Problem
In the example of the singleton pattern, use an instance counter to prove only one instance
is created. Recall that an instance counter is a private static variable that keeps track of the
number of instances a class has at any given moment.
3.5. Problem
How could we make multiple singletons if the default constructor was protected instead of
private?
3.6. Problem
We gave examples of association, creation, and aggregation relationships between a
framework class A, and an abstract class or template parameter B representing a class to be
specified later in a customization. Give a useful example where class A might be derived
from a template parameter B:
template <typename B> class A: public B { ... };
3.7. Problem
Re implement the adventure game framework to allow users to control one hero. For
example, the Hero::defend() function could accept keyboard input to determine how much
damage the hero does to the monster, and the Maze::action() function should allow the hero
to decide which room to enter. You may make any modifications you deem necessary, but
keep the framework general.
3-61
Pattern Oriented Programming with C++/Pearce
3.8. Problem: A Framework for Finite State Machines
Let FSM be the class of all finite state machines:
class FSM { ... };
A finite state machine, M, is a mathematical model of a simple computational device that is
always in one of finitely many states:
q0, q1, ... qmax
We will assume q0 is M's initial state. We will also assume one of M's states is designated
as a final state, qfin. For our purposes it is sufficient to assume states are simply consecutive
integers:
typedef int State;
M has a control function called run(), which expects a string as input and returns a bool:
bool FSM::run(const string& s);
The control function examines each character in s, once, from beginning to end. Examining
a character causes M to change state. The new state is given by a function called next():
/* c = current character
s = current statereturn value = new state
*/ State FSM::next(char c, State s);
If M is in the final state, qfin, after it examines the last character in s, then run() returns true
and we say M accepts s. Otherwise, run() returns false and we say M rejects s6.
M also has a user interface function called test(), which perpetually resets M's current state
to q0, prompts the user for an input string, s, then prints an acceptance message if run(s)
returns true, and a rejection message otherwise.
6 Intuitively, we can think of M's state, qi, as the current content of its RAM, run() is M's CPU (which runs the fetch-execute cycle), s is an input file, and next() is M's current program.
3-62
3. Frameworks, Kits, and Polymorphism
Here's a sample output using a three state machine, M. M is in state qi if it has examined the
character '0' k times, and k % 3 == i. M's final state is q0. Thus, M accepts s if and only if
the number of zeroes in s is divisible by 3:
current state = 0final state = 0enter input string: 011001state = 1state = 1state = 1state = 2state = 0state = 0String acceptedcurrent state = 0final state = 0enter input string: catfishstate = 0state = 0state = 0state = 0state = 0state = 0state = 0String acceptedcurrent state = 0final state = 0enter input string: 00001state = 1state = 2state = 0state = 1state = 1String rejected
Implement a framework for building finite state machines. There are two approaches:
Either next() can be specified in a user-defined derived class, or the user can pass a
function object (i.e., a functor) representing next() to the finite state machine constructor.
Use the first approach for this exercise. Of course run() and next() should throw exceptions
if they get into trouble, and test() should catch these exceptions.
Test your framework by building and testing a finite state machine that accepts all strings
that contain the same number of zeroes as ones.
3-63
Pattern Oriented Programming with C++/Pearce
3.9. Problem
Repeat the last problem, but use the second approach: the user specifies next() by passing a
function object to the FSM constructor. (Function objects were introduced in problem
2.12.).
3.10. Problem: Push Down Automata7
Obviously a finite state machine can be quite powerful, depending on the complexity of its
"program", the next() function. A push down automaton (PDA) is a finite state machine
with a simple next() function and a stack, which can hold an unlimited number of
characters8 but is initially empty:
stack<char> theStack;
Basically, next() is driven by a control table of the form:
Of course the table can have any number of rows. The rows shown are just sample entries.
The first row of the table can be interpreted as follows: if the character on top of the stack
is 'b', then next('a', 0) returns 1, pops 'b' off the stack, pushes 'c' onto the stack, then pushes
'a' onto the stack. The second entry means: if the character on top of the stack is 'c', then
next('a', 1) returns 1, and pops c off the stack. The third entry means: if the stack is empty,
then next('c', 1) returns 2 and doesn't do anything to the stack. (Equivalently, next() pops 'c'
off the stack, then pushes 'c' onto the stack.).
In addition to all of the user-specified states, a PDA has a special dead state, -1. The dead
state may never be a final state, and once a PDA enters the dead state, it's stuck there:
For all x, next(x, -1) = -1
7 See [LEW] for a discussion of the different types of finite state machines.8 Note that unlike the stack, the RAM only has finite capacity = max + 1.
3-64
3. Frameworks, Kits, and Polymorphism
Create a kit for building push down automata by deriving PDA from FSM.
class PDA: public FSM { ... };
Users only need to specify the control table in a formatted text file (you choose the format).
Internally, the control table should be represented as an STL map.
class Configuration { ... }; // current state, char, & topclass Action { ... }; // next state & push stringtypedef map<Configuration, Action> ControlTable;
Test your kit by building a PDA that accepts strings that have a prefix of n zeroes and a
suffix of n ones, where n can be any value.
3.11. Problem: Finite Automata
A Finite Automaton (FA) is a PDA with no stack (hence users only need to specify a three-
column control table). Create a kit for building finite automata by specializing the PDA
class from the last problem:
class FA: public PDA { ... };
Test your kit by building a finite automaton that accepts all strings containing an even
number of substrings of the form "01".
2.11. Problem: Demand Driven Pipeline Project
Repeat the data driven example using demand driven filters. Use the same test driver. The
only difference is that f1->start() is replaced by f4->start().
Obviously filters subscribe to their output pipes instead of their input pipes in a demand
driven pipeline. When a down stream filter reads from its input pipe, the pipe automatically
notifies the upstream filter that data is needed.
Two special problems arise in the demand driven case. First, what happens when data is
demanded from the output pipe of a tester? The tester needs to repeatedly read from its
input pipe until the pipeline is empty or until it receives a data item that passes its test and
can be written to its output pipe.
3-65
Pattern Oriented Programming with C++/Pearce
Second, how does a demand drive pipeline shut down? In the data driven case this was easy
because the producer controlled the while loop in start() and the producer knew when it
was finished producing data. When produce() returned true, the loop control variable was
set to true and the start() function, hence the pipeline, terminated. In a demand-driven
pipeline the consumer controls the while loop in start(). How can the producer signal the
consumer that it's done producing data? The answer is by throwing a ProducerQuitting
exception. This means the while loop in start() must contain a try-catch block:
while(!fail){
try{
???}catch(ProducerQuitting pqe){
fail = true;}
}
Try declaring ProducerQuitting as a derived class from the exception class defined in the
C++ standard library:
class ProducerQuitting: public exception {};
This could also be an inner class of PipelineKit.
2.12. Problem: Using Functors
A functor or function object, is an object that can be called like a function. This is
accomplished by making the function call operator a member function. For example:
class Square{public:
double operator()(double x) { return square(x); }// etc.
};
We can now define some Square functors:
Square f, g, h;
3-66
3. Frameworks, Kits, and Polymorphism
Like ordinary objects, these objects can be passed as parameters, returned as values, or
assigned to variables. But they can also be called like functions:
cout << g(2) * h(3) << endl; // prints 36
The C++ standard library supplies some predefined functor templates (you'll need to
include <functional>). For example:
pointer_to_unary_function<double, double> s(square);
creates a functor called s that implements operator() using the square function, just like f, g,
and h:
cout << s(7) <, endl; // prints 49
Using functors instead of function pointers makes our programs a bit more object oriented.
Re implement the pipeline toolkit to use functors to process messages instead of function
pointers.
Problem
Implement the data-driven Pipeline Toolkit. Test your implementation by constructing a
pipeline that reads a text file through standard input, eliminates all punctuation marks, and
writes the result to a file through standard output.
Problem
We can imitate LISP using the algorithms from the standard C++ library. Assume isEven()
and square() are ordinary global functions:
bool isEven(int x) { return x % 2 == 0; }int square(int x) { return x * x; }
The declarations of remove_if() and transform() are declared in the <algorithm> header
file:
3-67