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
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
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
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.