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

  }

  class BadShapeCreation : public exception {

    string reason;

  public:

    BadShapeCreation(string type) {

      reason = "Cannot create type " + type;

    }

    ~BadShapeCreation() throw() {}

    const char *what() const throw() {

      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() {

  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;

    for(int j = 0; j < shapes.size(); j++)

      delete shapes[j];

    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, 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 explicit: it’s a string that names the type. However, your scheme can use other information—for example, in a parser the output of the scanner can 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( ).

The default constructor in this scheme has at least one important chore—it must set the value of the s pointer to zero. This may sound 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).

The virtual constructor uses its argument to select the actual ("letter") object to construct, 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 functions 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.