Adding at least one virtual function to a class alters the storage layout for all objects of that class type. In this article, I begin to explain how C++ compilers typically implement virtual functions by explaining how the use of virtual functions affects the storage layout for objects.
My sample classes represent an assortment of two-dimensional geometric shapes such as circle, rectangle, and triangle, all derived from a common base class called "shape." All the shapes also have common attributes, such as position, and outline, and fill colors. Each derived class adds some linear or angular distance(s) to characterize the physical extent of the shape. For example, a circle has a radius, a rectangle has a height and a width, and a triangle has two sides and an angle.
The definition for the base class shape looks in part like:
class shape { public: shape(); // constructor virtual double area() const; virtual double perimeter() const; ~~~ private: coordinates position; color outline, fill; };
(As at least one reader observed earlier, the shape class should probably be an abstract base class with pure virtual functions. That's not relevant to this discussion. Ditto for virtual destructors.)
If the area and perimeter member functions weren't virtual, then the functions wouldn't occupy any storage within shape
objects. The storage for a shape
object would contain only the storage for the data members, as shown in Figure 1.
Figure 1: The storage layout of a shape object if classs shape
had no virtual functions.
However, shape
's area and perimeter member functions are virtual. A class type, such as shape
, with a least one virtual function is a polymorphic type. An object of polymorphic type is a polymorphic object. C++ compilers typically add a pointer to the storage layout of each polymorphic object. That pointer is commonly called a vptr
("VEE-pointer") and it points to a table of function pointers called a vtbl
("VEE-table").
C++ compilers don't create vptr
s and vtbl
s unless needed. Thus, non-polymorphic types don't have vptr
s and vtbl
s.
Each polymorphic type has its own vtbl
. That vtbl
contains one pointer for each virtual function in the class. For example, the shape
class has two virtual functions, area
and perimeter
, so the vtbl
for shape
contains one pointer to the shape
area
function and another to the perimeter
function, as shown in Figure 2.
Figure 2: The storage layout for shape
objects and shape
's vtbl
.
Figure 2 shows the vptr
situated at the beginning of each shape
. Some compilers place the vptr
after the last data member, instead. Each compiler can do as it pleases, as long as every object of a given polymorphic type has the same storage layout.
Each derived class inherits all the data members of its base class. Inherited members must have the same offsets within the derived class as they do in the base class. (This is true for classes with only one base class. It may not be true for classes with multiple base classes a complication that I'm going to ignore for now.)
A class derived from a polymorphic base class will be polymorphic as well, and it inherits the base class's vptr
. The vptr
must have the same offset in the base class subobject (the base class portion) of a derived class object as it does in a base class object.
For example, you can derive the circle class from shape as follows:
class circle: public shape { public: circle(double r); // constructor virtual double area() const; virtual double perimeter() const; ~~~ private: double radius; };
The circle
class is polymorphic and it defines its own versions of area
and perimeter
. The compiler generates a distinct vtbl
for class circle
. Except that each circle
object has the additional data member radius
, the storage layout for circle
objects, shown in Figure 3, is identical to the layout for shape
objects.
Figure 3: The storage layout for circle
objects and circle
's vtbl
.
As another example, here's class rectangle
derived from shape
:
class rectangle: public shape { public: rectangle(double h, double w); virtual double area() const; virtual double perimeter() const; ~~~ private: double height, width; };
Except that each rectangle
object has the additional data members height
and width
, the storage layout for rectangle
objects, shown in Figure 4, is identical to the layout for shape
objects.
Figure 4: The storage layout for rectangle
objects and rectangle
's vtbl
.
I'll have more to say in a future article about how you can emulate virtual functions in C.
Dan Saks is president of Saks & Associates, a C/C++ training and consulting company. This article is adapted from an article that first appeared on embedded.com.