Follow Techotopia on Twitter

On-line Guides
All Guides
eBook Store
iOS / Android
Linux for Beginners
Office Productivity
Linux Installation
Linux Security
Linux Utilities
Linux Virtualization
Linux Kernel
System/Network Admin
Programming
Scripting Languages
Development Tools
Web Development
GUI Toolkits/Desktop
Databases
Mail Systems
openSolaris
Eclipse Documentation
Techotopia.com
Virtuatopia.com
Answertopia.com

How To Guides
Virtualization
General System Admin
Linux Security
Linux Filesystems
Web Servers
Graphics & Desktop
PC Hardware
Windows
Problem Solutions
Privacy Policy

  




 

 

Thinking in C++ Vol 2 - Practical Programming
Prev Home Next

Multiple dispatching with Visitor

The goal of Visitor (the final, and arguably most complex, pattern in GoF) is to separate the operations on a class hierarchy from the hierarchy itself. This is quite an odd motivation because most of what we do in object-oriented programming is to combine data and operations into objects, and to use polymorphism to automatically select the correct variation of an operation, depending on the exact type of an object.

With Visitor you extract the operations from inside your class hierarchy into a separate, external hierarchy. The main hierarchy then contains a visit( ) function that accepts any object from your hierarchy of operations. As a result, you get two class hierarchies instead of one. In addition, you ll see that your main hierarchy becomes very brittle if you add a new class, you will force changes throughout the second hierarchy. GoF says that the main hierarchy should thus rarely change. This constraint is very limiting, and it further reduces the applicability of this pattern.

For the sake of argument, then, assume that you have a primary class hierarchy that is fixed; perhaps it s from another vendor and you can t make changes to that hierarchy. If you had the source code for the library, you could add new virtual functions in the base class, but this is, for some reason, not feasible. A more likely scenario is that adding new virtual functions is somehow awkward, ugly or otherwise difficult to maintain. GoF argues that distributing all these operations across the various node classes leads to a system that s hard to understand, maintain, and change. (As you ll see, Visitor can be much harder to understand, maintain and change.) Another GoF argument is that you want to avoid polluting the interface of the main hierarchy with too many operations (but if your interface is too fat, you might ask whether the object is trying to do too many things).

The library creator must have foreseen, however, that you will want to add new operations to that hierarchy, so that they can know to include the visit( ) function.

So (assuming you really need to do this) the dilemma is that you need to add member functions to the base class, but for some reason you can t touch the base class. How do you get around this?

Visitor builds on the double-dispatching scheme shown in the previous section. The Visitor pattern allows you to effectively extend the interface of the primary type by creating a separate class hierarchy of type Visitor to virtualize the operations performed on the primary type. The objects of the primary type simply accept the visitor and then call the visitor s dynamically bound member function. Thus, you create a visitor, pass it into the primary hierarchy, and you get the effect of a virtual function. Here s a simple example:

//: C10:BeeAndFlowers.cpp
// Demonstration of "visitor" pattern.
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
#include <ctime>
#include <cstdlib>
#include "../purge.h"
using namespace std;
 
class Gladiolus;
class Renuculus;
class Chrysanthemum;
 
class Visitor {
public:
virtual void visit(Gladiolus* f) = 0;
virtual void visit(Renuculus* f) = 0;
virtual void visit(Chrysanthemum* f) = 0;
virtual ~Visitor() {}
};
 
class Flower {
public:
virtual void accept(Visitor&) = 0;
virtual ~Flower() {}
};
 
class Gladiolus : public Flower {
public:
virtual void accept(Visitor& v) {
v.visit(this);
}
};
 
class Renuculus : public Flower {
public:
virtual void accept(Visitor& v) {
v.visit(this);
}
};
 
class Chrysanthemum : public Flower {
public:
virtual void accept(Visitor& v) {
v.visit(this);
}
};
 
// Add the ability to produce a string:
class StringVal : public Visitor {
string s;
public:
operator const string&() { return s; }
virtual void visit(Gladiolus*) {
s = "Gladiolus";
}
virtual void visit(Renuculus*) {
s = "Renuculus";
}
virtual void visit(Chrysanthemum*) {
s = "Chrysanthemum";
}
};
 
// Add the ability to do "Bee" activities:
class Bee : public Visitor {
public:
virtual void visit(Gladiolus*) {
cout << "Bee and Gladiolus << endl;
}
virtual void visit(Renuculus*) {
cout << "Bee and Renuculus << endl;
}
virtual void visit(Chrysanthemum*) {
cout << "Bee and Chrysanthemum << endl;
}
};
 
struct FlowerGen {
Flower* operator()() {
switch(rand() % 3) {
default:
case 0: return new Gladiolus;
case 1: return new Renuculus;
case 2: return new Chrysanthemum;
}
}
};
 
int main() {
srand(time(0)); // Seed the random number generator
vector<Flower*> v(10);
generate(v.begin(), v.end(), FlowerGen());
vector<Flower*>::iterator it;
// It's almost as if I added a virtual function
// to produce a Flower string representation:
StringVal sval;
for(it = v.begin(); it != v.end(); it++) {
(*it)->accept(sval);
cout << string(sval) << endl;
}
// Perform "Bee" operation on all Flowers:
Bee bee;
for(it = v.begin(); it != v.end(); it++)
(*it)->accept(bee);
purge(v);
} ///:~
 

Flower is the primary hierarchy, and each subtype of Flower can accept( ) a Visitor. The Flower hierarchy has no operations other than accept( ), so all the functionality of the Flower hierarchy is contained in the Visitor hierarchy. Note that the Visitor classes must know about all the specific types of Flower, and if you add a new type of Flower the entire Visitor hierarchy must be reworked.

The accept( ) function in each Flower begins a double dispatch as described in the previous section. The first dispatch determines the exact type of Flower and the second determines the exact type of Visitor. Once you know the exact types you can perform an operation appropriate to both.

It s very unlikely that you ll use Visitor because its motivation is unusual and its constraints are stultifying. The GoF examples are not convincing the first is a compiler (not many people write compilers, and it seems quite rare that Visitor is used within these compilers), and they apologize for the other examples, saying you wouldn t actually use Visitor for anything like this. You would need a stronger compulsion than that presented in GoF to abandon an ordinary OO structure for Visitor what benefit does it really buy you in exchange for much greater complexity and constraint? Why can t you simply add more virtual functions in the base class when you discover you need them? Or, if you really need to paste new functions into an existing hierarchy and you are unable to modify that hierarchy, why not try multiple inheritance first? (Even then, the likelihood of saving the existing hierarchy this way is slim). Consider also that, to use Visitor, the existing hierarchy must incorporate a visit( ) function from the beginning, because to add it later would mean that you had permission to modify the hierarchy, so you could just add ordinary virtual functions as you need them. No, Visitor must be part of the architecture from the beginning, and to use it requires a motivation greater than that in GoF.[147]

We present Visitor here because we have seen it used when it shouldn t be, just as multiple inheritance and any number of other approaches have been used inappropriately. If you find yourself using Visitor, ask why. Are you really unable to add new virtual functions in the base class? Do you really want to be restricted from adding new types in your primary hierarchy?

Thinking in C++ Vol 2 - Practical Programming
Prev Home Next

 
 
   Reproduced courtesy of Bruce Eckel, MindView, Inc. Design by Interspire