Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |
When dealing with multiple interacting types, a program can
get particularly messy. For example, consider a system that parses and executes
mathematical expressions. You want to be able to say Number + Number, Number
* Number, and so on, where Number is the base class for a family of
numerical objects. But when you say a + b, and you don t know the exact
type of either a or b, how can you get them to interact properly?
The answer starts with something you probably don t think
about: C++ performs only single dispatching. That is, if you are performing an
operation on more than one object whose type is unknown, C++ can invoke the
dynamic binding mechanism on only one of those types. This doesn t solve the
problem described here, so you end up detecting some types manually and
effectively producing your own dynamic binding behavior.
The solution is called multiple dispatching (described in GoF in the context of the Visitor pattern, shown in the next section). Here,
there will be only two dispatches, which is referred to as double dispatching. Remember that polymorphism can occur only via virtual function calls,
so if you want multiple dispatching to occur, there must be a virtual function
call to determine each unknown type. Thus, if you are working with two
different type hierarchies that are interacting, you ll need a virtual call in
each hierarchy. Generally, you ll set up a configuration such that a single
member function call generates more than one virtual member function call and
thus determines more than one type in the process: you ll need a virtual
function call for each dispatch. The virtual functions in the following example
are called compete( ) and eval( ) and are both members
of the same type (this is not a requirement for multiple dispatching):
//: C10:PaperScissorsRock.cpp
// Demonstration of multiple dispatching.
#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>
#include <ctime>
#include <cstdlib>
#include "../purge.h"
using namespace std;
class Paper;
class Scissors;
class Rock;
enum Outcome { WIN, LOSE, DRAW };
ostream& operator<<(ostream& os, const
Outcome out) {
switch(out) {
default:
case WIN: return os << "win";
case LOSE: return os << "lose";
case DRAW: return os << "draw";
}
}
class Item {
public:
virtual Outcome compete(const Item*) = 0;
virtual Outcome eval(const Paper*) const = 0;
virtual Outcome eval(const Scissors*) const= 0;
virtual Outcome eval(const Rock*) const = 0;
virtual ostream& print(ostream& os) const =
0;
virtual ~Item() {}
friend ostream& operator<<(ostream& os,
const Item* it) {
return it->print(os);
}
};
class Paper : public Item {
public:
Outcome compete(const Item* it) { return
it->eval(this);}
Outcome eval(const Paper*) const { return DRAW; }
Outcome eval(const Scissors*) const { return WIN; }
Outcome eval(const Rock*) const { return LOSE; }
ostream& print(ostream& os) const {
return os << "Paper ";
}
};
class Scissors : public Item {
public:
Outcome compete(const Item* it) { return
it->eval(this);}
Outcome eval(const Paper*) const { return LOSE; }
Outcome eval(const Scissors*) const { return DRAW; }
Outcome eval(const Rock*) const { return WIN; }
ostream& print(ostream& os) const {
return os << "Scissors";
}
};
class Rock : public Item {
public:
Outcome compete(const Item* it) { return
it->eval(this);}
Outcome eval(const Paper*) const { return WIN; }
Outcome eval(const Scissors*) const { return LOSE; }
Outcome eval(const Rock*) const { return DRAW; }
ostream& print(ostream& os) const {
return os << "Rock ";
}
};
struct ItemGen {
Item* operator()() {
switch(rand() % 3) {
default:
case 0: return new Scissors;
case 1: return new Paper;
case 2: return new Rock;
}
}
};
struct Compete {
Outcome operator()(Item* a, Item* b) {
cout << a << "\t" << b
<< "\t";
return a->compete(b);
}
};
int main() {
srand(time(0)); // Seed the random number generator
const int sz = 20;
vector<Item*> v(sz*2);
generate(v.begin(), v.end(), ItemGen());
transform(v.begin(), v.begin() + sz,
v.begin() + sz,
ostream_iterator<Outcome>(cout,
"\n"),
Compete());
purge(v);
} ///:~
Outcome categorizes the different possible results of
a compete( ), and the operator<< simplifies the
process of displaying a particular Outcome.
Item is the base class for the types that will be
multiply-dispatched. Compete::operator( ) takes two Item*
(the exact type of both are unknown) and begins the double-dispatching process
by calling the virtual Item::compete( ) function. The virtual
mechanism determines the type a, so it wakes up inside the compete( )
function of a s concrete type. The compete( ) function
performs the second dispatch by calling eval( ) on the remaining
type. Passing itself (this) as an argument to eval( ) produces
a call to the overloaded eval( ) function, thus preserving the type
information of the first dispatch. When the second dispatch is completed, you
know the exact types of both Item objects.
In main( ), the STL algorithm generate( )
populates the vector v, then transform( ) applies Compete::operator( )
to the two ranges. This version of transform( ) takes the start and
end point of the first range (containing the left-hand Items used in the
double dispatch); the starting point of the second range, which holds the
right-hand Items; the destination iterator, which in this case is
standard output; and the function object (a temporary of type Compete)
to call for each object.
It requires a lot of ceremony to set up multiple
dispatching, but keep in mind that the benefit is the syntactic elegance
achieved when making the call instead of writing awkward code to determine the
type of one or more objects during a call, you simply say: You two! I don t
care what types you are, interact properly with each other! Make sure this
kind of elegance is important to you before embarking on multiple dispatching,
however.
Note that multiple dispatching is, in effect, performing a
table lookup. Here, the lookup is performed using virtual functions, but you
could instead perform a literal table lookup. With more than a few dispatches
(and if you are prone to making additions and changes), a table lookup may be a
better solution to the problem.
Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |