Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
B.Eckel - Thinking in C++, Vol.2, 2nd edition.pdf
Скачиваний:
50
Добавлен:
08.05.2013
Размер:
2.09 Mб
Скачать

g2(new KillAndDismember); g1.play();

g2.play(); } ///:~

In this environment, Player objects interact with Obstacle objects, but there are different types of players and obstacles depending on what kind of game you’re playing. You determine the kind of game by choosing a particular GameElementFactory, and then the GameEnvironment controls the setup and play of the game. In this example, the setup and play is very simple, but those activities (the initial conditions and the state change) can determine much of the game’s outcome. Here, GameEnvironment is not designed to be inherited, although it could very possibly make sense to do that.

This also contains examples of Double Dispatching and the Factory Method, both of which will be explained later.

Virtual constructors

One of the primary goals of using a factory is so that you can organize your code so you don’t have to select an exact type of constructor when creating an object. That is, you can say, “I don’t know precisely what type of object you are, but here’s the information: Create yourself.”

In addition, during a constructor call the virtual mechanism does not operate (early binding occurs). Sometimes this is awkward. For example, in the Shape program it seems logical that inside the constructor for a Shape object, you would want to set everything up and then draw( ) the Shape. draw( ) should be a virtual function, a message to the Shape that it should draw itself appropriately, depending on whether it is a circle, square, line, and so on. However, this doesn’t work inside the constructor, for the reasons given in Chapter XX: Virtual functions resolve to the “local” function bodies when called in constructors.

If you want to be able to call a virtual function inside the constructor and have it do the right thing, you must use a technique to simulate a virtual constructor (which is a variation of the Factory Method). This is a conundrum. Remember the idea of a virtual function is that you send a message to an object and let the object figure out the right thing to do. But a constructor builds an object. So a virtual constructor would be like saying, “I don’t know exactly what type of object you are, but build yourself anyway.” In an ordinary constructor, the compiler must know which VTABLE address to bind to the VPTR, and if it existed, a virtual constructor couldn’t do this because it doesn’t know all the type information at compile-time. It makes sense that a constructor can’t be virtual because it is the one function that absolutely must know everything about the type of the object.

And yet there are times when you want something approximating the behavior of a virtual constructor.

In the Shape example, it would be nice to hand the Shape constructor some specific information in the argument list and let the constructor create a specific type of Shape (a Circle, Square) with no further intervention. Ordinarily, you’d have to make an explicit call to the Circle, Square constructor yourself.

Chapter 16: Design Patterns

444

Coplien26 calls his solution to this problem “envelope and letter classes.” The “envelope” class is the base class, a shell that contains a pointer to an object of the base class. The constructor for the “envelope” determines (at runtime, when the constructor is called, not at compile-time, when the type checking is normally done) what specific type to make, then creates an object of that specific type (on the heap) and assigns the object to its pointer. All the function calls are then handled by the base class through its pointer. So the base class is acting as a proxy for the derived class:

//: C09:VirtualConstructor.cpp #include <iostream>

#include <string> #include <exception> #include <vector> using namespace std;

class Shape { Shape* s;

// Prevent copy-construction & operator= Shape(Shape&);

Shape operator=(Shape&); protected:

Shape() { s = 0; }; public:

virtual void draw() { s->draw(); } virtual void erase() { s->erase(); } virtual void test() { s->test(); }; virtual ~Shape() {

cout << "~Shape\n"; if(s) {

cout << "Making virtual call: "; s->erase(); // Virtual call

}

cout << "delete s: ";

delete s; // The polymorphic deletion

}

class BadShapeCreation : public exception { string reason;

public:

BadShapeCreation(string type) {

reason = "Cannot create type " + type;

}

const char *what() const {

26James O. Coplien, Advanced C++ Programming Styles and Idioms, Addison-Wesley, 1992.

Chapter 16: Design Patterns

445

return reason.c_str();

}

};

Shape(string type) throw(BadShapeCreation);

};

class Circle : public Shape { Circle(Circle&);

Circle operator=(Circle&); Circle() {} // Private constructor friend class Shape;

public:

void draw() { cout << "Circle::draw\n"; } void erase() { cout << "Circle::erase\n"; } void test() { draw(); }

~Circle() { cout << "Circle::~Circle\n"; }

};

class Square : public Shape { Square(Square&);

Square operator=(Square&); Square() {}

friend class Shape; public:

void draw() { cout << "Square::draw\n"; } void erase() { cout << "Square::erase\n"; } void test() { draw(); }

~Square() { cout << "Square::~Square\n"; }

};

Shape::Shape(string type) throw(Shape::BadShapeCreation) { if(type == "Circle")

s = new Circle;

else if(type == "Square") s = new Square;

else throw BadShapeCreation(type);

draw(); // Virtual call in the constructor

}

char* shlist[] = { "Circle", "Square", "Square", "Circle", "Circle", "Circle", "Square", "" };

int main() {

Chapter 16: Design Patterns

446

vector<Shape*> shapes;

cout << "virtual constructor calls:" << endl; try {

for(char** cp = shlist; **cp; cp++) shapes.push_back(new Shape(*cp));

}catch(Shape::BadShapeCreation e) { cout << e.what() << endl;

return 1;

}

for(int i = 0; i < shapes.size(); i++) { shapes[i]->draw();

cout << "test\n"; shapes[i]->test(); cout << "end test\n"; shapes[i]->erase();

}

Shape c("Circle"); // Create on the stack cout << "destructor calls:" << endl; for(int j = 0; j < shapes.size(); j++) {

delete shapes[j];

cout << "\n------------\n";

}

} ///:~

The base class Shape contains a pointer to an object of type Shape as its only data member. When you build a “virtual constructor” scheme, you must exercise special care to ensure this pointer is always initialized to a live object.

Each time you derive a new subtype from Shape, you must go back and add the creation for that type in one place, inside the “virtual constructor” in the Shape base class. This is not too onerous a task, but the disadvantage is you now have a dependency between the Shape class and all classes derived from it (a reasonable trade-off, it seems). Also, because it is a proxy, the base-class interface is truly the only thing the user sees.

In this example, the information you must hand the virtual constructor about what type to create is very explicit: It’s a string that names the type. However, your scheme may use other information – for example, in a parser the output of the scanner may be handed to the virtual constructor, which then uses that information to determine which token to create.

The virtual constructor Shape(type) can only be declared inside the class; it cannot be defined until after all the derived classes have been declared. However, the default constructor can be defined inside class Shape, but it should be made protected so temporary Shape objects cannot be created. This default constructor is only called by the constructors of derived-class objects. You are forced to explicitly create a default constructor because the compiler will create one for you automatically only if there are no constructors defined. Because you must define Shape(type), you must also define Shape( ).

Chapter 16: Design Patterns

447

The default constructor in this scheme has at least one very important chore – it must set the value of the s pointer to zero. This sounds strange at first, but remember that the default constructor will be called as part of the construction of the actual object – in Coplien’s terms, the “letter,” not the “envelope.” However, the “letter” is derived from the “envelope,” so it also inherits the data member s. In the “envelope,” s is important because it points to the actual object, but in the “letter,” s is simply excess baggage. Even excess baggage should be initialized, however, and if s is not set to zero by the default constructor called for the “letter,” bad things happen (as you’ll see later).

The virtual constructor takes as its argument information that completely determines the type of the object. Notice, though, that this type information isn’t read and acted upon until runtime, whereas normally the compiler must know the exact type at compile-time (one other reason this system effectively imitates virtual constructors).

Inside the virtual constructor there’s a switch statement that uses the argument to construct the actual (“letter”) object, which is then assigned to the pointer inside the “envelope.” At that point, the construction of the “letter” has been completed, so any virtual calls will be properly directed.

As an example, consider the call to draw( ) inside the virtual constructor. If you trace this call (either by hand or with a debugger), you can see that it starts in the draw( ) function in the base class, Shape. This function calls draw( ) for the “envelope” s pointer to its “letter.” All types derived from Shape share the same interface, so this virtual call is properly executed, even though it seems to be in the constructor. (Actually, the constructor for the “letter” has already completed.) As long as all virtual calls in the base class simply make calls to identical virtual function through the pointer to the “letter,” the system operates properly.

To understand how it works, consider the code in main( ). To fill the vector shapes, “virtual constructor” calls are made to Shape. Ordinarily in a situation like this, you would call the constructor for the actual type, and the VPTR for that type would be installed in the object. Here, however, the VPTR used in each case is the one for Shape, not the one for the specific

Circle, Square, or Triangle.

In the for loop where the draw( ) and erase( ) functions are called for each Shape, the virtual function call resolves, through the VPTR, to the corresponding type. However, this is Shape in each case. In fact, you might wonder why draw( ) and erase( ) were made virtual at all. The reason shows up in the next step: The base-class version of draw( ) makes a call, through the “letter” pointer s, to the virtual function draw( ) for the “letter.” This time the call resolves to the actual type of the object, not just the base class Shape. Thus the runtime cost of using virtual constructors is one more virtual call every time you make a virtual function call.

In order to create any function that is overridden, such as draw( ), erase( ) or test( ), you must proxy all calls to the s pointer in the base class implementation, as shown above. This is because, when the call is made, the call to the envelope’s member function will resolve as being to Shape, and not to a derived type of Shape. Only when you make the proxy call to s will the virtual behavior take place. In main( ), you can see that everything works correctly, even when calls are made inside constructors and destructors.

Chapter 16: Design Patterns

448

Соседние файлы в предмете Численные методы
  • #
    08.05.20133.99 Mб22A.Menezes, P.van Oorschot,S.Vanstone - HANDBOOK OF APPLIED CRYPTOGRAPHY.djvu
  • #
  • #
    08.05.20135.91 Mб24B.Eckel - Thinking in Java, 3rd edition (beta).pdf
  • #
  • #
    08.05.20136.09 Mб17D.MacKay - Information Theory, Inference, and Learning Algorithms.djvu
  • #
    08.05.20133.85 Mб15DIGITAL Visual Fortran ver.5.0 - Programmers Guide to Fortran.djvu
  • #
    08.05.20131.84 Mб12E.A.Lee, P.Varaiya - Structure and Interpretation of Signals and Systems.djvu