Выбрать главу

class Obstacle {

public:

  virtual void action() = 0;

};

class Player {

public:

  virtual void interactWith(Obstacle*) = 0;

};

class Kitty: public Player {

  virtual void interactWith(Obstacle* ob) {

    cout << "Kitty has encountered a ";

    ob->action();

  }

};

class KungFuGuy: public Player {

  virtual void interactWith(Obstacle* ob) {

    cout << "KungFuGuy now battles against a ";

    ob->action();

  }

};

class Puzzle: public Obstacle {

public:

  void action() { cout << "Puzzle\n"; }

};

class NastyWeapon: public Obstacle {

public:

  void action() { cout << "NastyWeapon\n"; }

};

// The abstract factory:

class GameElementFactory {

public:

  virtual Player* makePlayer() = 0;

  virtual Obstacle* makeObstacle() = 0;

};

// Concrete factories:

class KittiesAndPuzzles :

  public GameElementFactory {

public:

  virtual Player* makePlayer() {

    return new Kitty;

  }

  virtual Obstacle* makeObstacle() {

    return new Puzzle;

  }

};

class KillAndDismember :

  public GameElementFactory {

public:

  virtual Player* makePlayer() {

    return new KungFuGuy;

  }

  virtual Obstacle* makeObstacle() {

    return new NastyWeapon;

  }

};

class GameEnvironment {

  GameElementFactory* gef;

  Player* p;

  Obstacle* ob;

public:

  GameEnvironment(GameElementFactory* factory) :

    gef(factory), p(factory->makePlayer()),

    ob(factory->makeObstacle()) {}

  void play() {

    p->interactWith(ob);

  }

  ~GameEnvironment() {

    delete p;

    delete ob;

    delete gef;

  }

};

int main() {

  GameEnvironment

    g1(new KittiesAndPuzzles),

    g2(new KillAndDismember);

  g1.play();

  g2.play();

}

/* Output:

Kitty has encountered a Puzzle

KungFuGuy now battles against a NastyWeapon */ ///:~

In this environment, Player objects interact with Obstacle objects, but the types of players and obstacles depend on the game. 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 are 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 possibly make sense to do that.

This example also illustrates double dispatching, which will be explained later.

Virtual constructors

One of the primary goals of using a factory is to 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. The draw( ) function should be a virtual function, a message to the Shape that it should draw itself appropriately, depending on whether it is a circle, a square, a line, and so on. However, this doesn’t work inside the constructor, 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.

Coplien[122] 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, creates an object of that specific type (on the heap), and then 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:

//: C10: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 calclass="underline" ";

      s->erase(); // Virtual call

    }

    cout << "delete s: ";

    delete s; // The polymorphic deletion

вернуться

122

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