IntroductionVirtual functions are one of the most interesting and useful features of classes in C++. They allow for thinking of an object in terms of what type it is (Apple) as well as what category of types it belongs with (Fruit). Further, virtual functions allow for operating on objects in ways that respect their actual types while refering to them by category. However, the power and flexibility of virtual functions comes at a price that a good programmer must weigh against the benefits. Let's take a quick look at using virtual functions and abstract base classes and from there examine a way in which we can improve program performance while retaining the power and flexbility of virtual functions. Along with modeling different kinds of fruit and different kinds of animals, modeling different kinds of shapes accounts for most of the polymorphism examples found in C++ textbooks. More importantly, modeling different kinds of shapes readily lends itself to an area of game programming where improving performance is a high priority, namely graphics rendering. So modeling shapes will make a good basis for our examination. Now, let's begin.
class Shape
{
public:
Shape()
{
}
virtual ~Shape()
{
}
virtual void DrawOutline() const = 0;
virtual void DrawFill() const = 0;
};
class Rectangle : public Shape
{
public:
Rectangle()
{
}
virtual ~Rectangle()
{
}
virtual void DrawOutline() const
{
...
}
virtual void DrawFill() const
{
...
}
};
class Circle : public Shape
{
public:
Circle()
{
}
virtual ~Circle()
{
}
virtual void DrawOutline() const
{
...
}
virtual void DrawFill() const
{
...
}
};
All good so far, right? We can, for example, write... Shape *myShape = new Rectangle; myShape->DrawOutline(); delete myShape; ...and trust C++'s runtime polymorphism to decide that the myShape pointer actually points to a Rectangle and that Rectangle's DrawOutline() method should be called. If we wanted it to be a circle instead, we could just change "new Rectangle" to "new Circle", and Circle's DrawOutline() method would be called instead. But wait a second. Thanks, C++, for the runtime polymorphism, but it's pretty obvious from looking at that code that myShape is going to be a Rectangle; we don't need fancy vtables to figure that out. Consider this code:
void DrawAShapeOverAndOver(Shape* myShape)
{
for(int i=0; i<10000; i++)
{
myShape->DrawOutline();
}
}
Shape *myShape = new Rectangle;
DrawAShapeOverAndOver(myShape);
delete myShape;
Look at what happens there! The program picks up myShape, inspects it, and says "Hmm, a Rectangle. Ok." Then it puts it down. Then it picks it up again. "Hmm. This time, it's a Rectangle. Ok. Hmm, and this time it's a... Rectangle. Ok." Repeat 9,997 times. Does all this type inspection eat up CPU cycles? Darn tootin' it does. Although virtual function calls aren't what you'd call slow, even a small delay really starts to add up when you're doing it 10,000 times per object per frame. The real tragedy here is that we know that the program doesn't really need to check myShape's type each time through the loop. "It's always going to be the same thing!", we shout at the compiler, "Just have the program look it up the first time!" For that matter, it doesn't really need to be looked up the first time. Because we are calling it on a Rectangle that we have just created, the object type is still going to be a Rectangle when DrawAShapeOverAndOver() gets to it. Let's see if we can rewrite this function in a way that doesn't require runtime lookups. We will make it specifically for Rectangles, so we can just flat-out tell the dumb compiler what it is and forego the lookup code.
void DrawAShapeWhichIsARectangleOverAndOver(Shape* myShape)
{
for(int i=0; i<10000; i++)
{
reinterpret_cast<Rectangle*>(myShape)->DrawOutline();
}
}
Unfortunately, this doesn't help one bit. Telling the compiler that the object is a Rectangle isn't enough. For all the compiler knows, the object could be a subclass of Rectangle. We still haven't prevented the compiler from inserting runtime lookup code. To do that we must remove the virtual keyword from the declaration of DrawOutline() and thereby change it into a non-virtual function. That means in turn, however, that we have to declare a separate DrawAShapeOverAndOver() for each and every subclass of Shape that we might want to draw. Alas, pursuing our desire for efficiency has driven us further and further away from our goal, to the point where there is barely any polymorphism left at all. So sad. |
|