TOC PREV NEXT INDEX

C++: A Dialog


10.3. Introduction to Polymorphism


To select the correct function to be called based on the actual type of an object at run time, we have to use polymorphism. Polymorphic behavior of our StockItem and DatedStockItem classes means that we can (for example) mix StockItem and DatedStockItem objects in a Vec and have the right Reorder function executed for each object in the Vec.
Susan wanted to know the motivation for using polymorphism here:
Susan: Why would you want to handle several different types of data as though they were the same type?
Steve: Because the objects of these two classes perform the same operation, although in a slightly different way, which is why they can have the same interface. In our example, a DatedStockItem acts just like a StockItem except that it has an additional data field and produces different reordering information. Ideally, we would be able to mix these two types in the application program without having to worry about which class each object belongs to except when creating an individual item (at which time we have to know whether the item has an expiration date).
Susan: Yes, but I don't understand why we need to do this in the first place. Why don't we just have two Vecs, one for the StockItem objects and one for the DatedStockItem objects?
Steve: Yes, it would be possible to do that. But it would make the program more complicated and wouldn't allow for adding further derived classes in a simple way. Imagine how messy the program would be if we had 10 derived classes instead of just one!
Susan also had a question about the relationship between base and derived classes:
Susan: What do the base and derived classes share besides an interface?
Steve: The derived class contains all of the member variables of the base class and can access those member variables or call any of the member functions of the base class, if they are public or protected. Of course, the derived class can also add whatever new member functions and member variables it needs.
However, there is a serious complication in using polymorphism: we have to refer to the objects via a pointer rather than directly.1 While C++ does have a "native" means of doing this, it exposes us to all the dangers of pointers, both those that you're already acquainted with and others that we'll get to later in this chapter.
Susan wanted more details on why pointers are dangerous; here's the first installment of our discussion of this point.
Susan: You keep saying that pointers are dangerous; what do they do that is so dangerous?
Steve: It's not what they do but what their users do: mostly, create memory leaks and dangling pointers (which point to memory that has already been freed).
Susan: So pointers are dangerous because it is just too easy to make mistakes when you use them?
Steve: Yes. In theory, pointers are fine, which is probably why they're so popular in computer science courses. In practice, however, they are very error-prone.
The ideal solution to this problem is to confine pointers to the interior of classes we design so that we can keep track of them ourselves and let the application programmer worry about getting the job done. As it happens, this is possible; thus, we can obtain the benefits of polymorphism without exposing the application programmer (as opposed to the class designers; i.e., us) to the hazards of pointers. We'll see how to do that later in this chapter.
But before investigating that more sophisticated method of providing polymorphism, we need to understand the workings of the native polymorphism mechanism in C++. As we saw in Chapter 9, the address of a derived class object can be assigned to a pointer declared to be a pointer to a base class of that derived class. While this does not by itself solve the problem of calling the correct function in these circumstances, there is a way to get the behavior we want. If we define a special kind of function called a virtual function and refer to it through a pointer (or a reference) to an object, the version of that function to be executed will be determined by the actual type of the object to which the pointer (or reference) refers, rather than by the declared type of the pointer (or reference). This implies that if we declare a function to be virtual, when a function with that signature is called via a base class pointer, the actual function to be called is selected at run time rather than at compile time, as happens with non-virtual functions. Clearly, if the actual run-time type of the object determines which version of the function is called, the compiler can't select the function at compile time.
Because the determination of the function to be called is delayed until run time, the compiler has to add code to each function call to make that determination. This code uses a construct called a vtable to keep track of the locations of all the functions for a given type of object so that the compiler-generated code can find the right function when the call is about to be executed.
As you might imagine, Susan had some questions about this notion of virtual function calls. Here's the beginning of that discussion:
Susan: I don't understand how the function to be executed is selected.
Steve: The mechanism depends on whether it is a virtual function. If not, the linker can figure out the exact address of the function when it is linking the program, because the type of the pointer (which is known at compile time) is used to determine which function will be called. On the other hand, with a virtual function declaration, the function to be executed depends on the actual type of the object pointed to rather than the type of the pointer to the object; since that information can't be known at compile time, the linker can't make the determination of which function to call. Therefore, in such cases, the compiler sticks code in the executable program that figures it out at run time by consulting the vtable for the particular type of object the base class pointer refers to.

The virtual Keyword

But exactly how does this help us with our Reorder function? Let's see how a virtual function affects the behavior of our final example program from Chapter 9 (nvirtual.cpp, Figure 9.39). Figure 10.1 shows the same interface as before, except that StockItem::Reorder is declared to be virtual.2 Because the current test program (virtual.cpp) and implementation file (itemb.cpp) are almost identical to the final test program (nvirtual.cpp) and implementation file (itema.cpp) in Chapter 9, differing only in that the new ones #include "itemb.h" rather than "itema.h", I haven't reproduced the new versions of those files.
If you printed out the corresponding files from the previous chapter, you might just want to mark them up to indicate these changes. Otherwise, I strongly recommend that you print out the files that contain this interface and its implementation, as well as the test program, for reference as you go through this section of the chapter; those files are itemb.h, itemb.cpp, and virtual.cpp, respectively.
FIGURE 10.1. Dangerous polymorphism: Interfaces of StockItem and DatedStockItem with virtual Reorder function (code\itemb.h)
// itemb.h

class StockItem
{
public:
StockItem(std::string Name, short InStock, short MinimumStock);
virtual void Reorder(std::ostream& os);

protected:
std::string m_Name;
short m_InStock;
short m_MinimumStock;
};

class DatedStockItem: public StockItem // deriving a new class
{
public:
DatedStockItem(std::string Name, short InStock, short MinimumStock,
std::string Expires);

virtual void Reorder(std::ostream& os);

protected:
static std::string Today();

protected:
std::string m_Expires;
};


Figure 10.2 shows the output of the new test program.
FIGURE 10.2. virtual function call example output (code\virtual.out)
StockItem::Reorder says:
Reorder 68 units of soup

DatedStockItem::Reorder says:
Return 10 units of milk
StockItem::Reorder says:
Reorder 15 units of milk

StockItem::Reorder says:
Reorder 70 units of beans

DatedStockItem::Reorder says:
Return 22 units of ham
StockItem::Reorder says:
Reorder 30 units of ham

DatedStockItem::Reorder says:
Return 90 units of steak
StockItem::Reorder says:
Reorder 95 units of steak


Notice that the output of this program is exactly the same as the output of the previous test program (Figure 9.39 on page 647), except for the last entry. With the non-virtual Reorder function in the previous program, we got the following output:
StockItem::Reorder says:
Reorder 5 units of steak

whereas with our virtual Reorder function, we get this output:
DatedStockItem::Reorder says:
Return 90 units of steak

StockItem::Reorder says:
Reorder 95 units of steak

According to our rules, the correct answer is 95 units of steak because the stock has expired, so the program that uses the virtual Reorder function works correctly while the previous one didn't. Why is this? Because when we call a virtual function through a base class pointer, the function executed is the one defined in the class of the actual object to which the pointer points, not the one defined in the class of the pointer.
To see how this works, let's start by looking at the way in which the layout of an object with virtual functions differs from that of a "normal" object. First, Figure 10.3 shows a possible memory representation of a simplified StockItem without virtual functions.
One of the interesting points about this figure is that there is no connection at run time between the StockItem object and its functions. Such a connection is unnecessary because the compiler can tell exactly which function will be called whenever a function is referenced for this object, whether directly or through a pointer, and therefore can provide the linker with enough information to generate a call directly to the appropriate function.
FIGURE 10.3. A simplified StockItem object without virtual functions
The situation is different if we have virtual functions. In that case, the compiler can't determine exactly which function will be called for an object pointed to by a StockItem* because the actual object may be a descendant of StockItem rather than an actual StockItem. If so, we want the function defined in the derived class (e.g., DatedStockItem) to be called even though the pointer is declared to point to an object of the base class (e.g., StockItem).
Since the actual type of the object for which we want to call the function isn't available at compile time, another way must be found to determine which function should be called. The most logical place to store this information is in the object itself, because after all, we need to know where the object is in order to call the function for it. In fact, an object of a class for which any virtual functions are declared does have an extra data item in it for exactly this purpose. So whenever a call to a virtual function is compiled, the compiler translates that call into instructions that use the information in the object to determine at run time which version of the virtual function will be called.
Here's the next installment of my discussion with Susan on the topic of virtual functions:
Susan: So, is a virtual function polymorphism?
Steve: Not quite. You need virtual functions to implement polymorphism in C++, but they're not the same thing.
Susan: Where in the definition of Reorder does it say it's virtual? The implementation file is the same as it was before.
Steve: It's in the declaration of Reorder in the interface of the StockItem class in the itemb.h header file: virtual void Reorder(ostream& os);. I've also repeated it in the derived class function declaration even though that's not strictly necessary. After a function is declared as virtual in a base class, we don't have to say it's virtual in the derived class or classes; the rule is "once virtual, always virtual".
If every object needed to contain the addresses of all its virtual functions, objects might be a lot larger than they would otherwise have to be. However, this is not necessary because all objects of the same class have the same virtual functions. Therefore, the addresses of all of the virtual functions for a given class are stored in a virtual function address table, or vtable for short, and every object of that class contains the address of the vtable for that class.
Given this description of the vtable, if we make the Reorder function virtual, a StockItem object will look something like Figure 10.4, and a DatedStockItem will resemble Figure 10.5.3
Susan had some more questions about vtables:
Susan: Are vtables customized for each class?
Steve: Yes.
Susan: Where do they come from, how are they created, and how do they do what they do?
Steve: The linker creates them based on instructions from the compiler after the compiler examines the class definition. All they do is store the addresses of the virtual functions for that class so that the compiler can generate code that will select the correct function for the object being referred to at run time.
FIGURE 10.4. Dangerous polymorphism: A simplified StockItem object with a virtual function
Susan: How is this different from derivation?
Steve: It's part of making derivation work correctly when we want to use pointers to the base class, and mix base and derived class objects in our program.
Susan: I don't get this vtable stuff. Does it just point the Reorder function in the proper direction at run time?
Steve: Not exactly. It allows the program to pick the correct Reorder function at run time.
Susan: This stuff is beyond "UGH!". It is just outrageous.
Steve: It wasn't that easy for me either. Acquiring a full understanding of virtual functions is one of the major milestones in learning C++, even for programmers with substantial experience in other languages.
FIGURE 10.5. Dangerous polymorphism: A simplified DatedStockItem object with a virtual function

Now that we have declared Reorder as a virtual function, let's see how this affects the operation of the function call examples we saw in Chapter 9 (Figures 9.36 through 9.38). First, Figure 10.6 shows how a virtual (i.e., dynamically determined) function call works when Reorder is called for a StockItem object through a StockItem pointer such as SIPtr.
FIGURE 10.6. Dangerous polymorphism: Calling a virtual Reorder function through a StockItem pointer to a StockItem object
The net result of the call illustrated in Figure 10.6 is the same as that illustrated in Figure 9.36: StockItem::Reorder is called, which is correct in this situation. Next, Figure 10.7 shows a virtual call for a DatedStockItem object through a DatedStockItem pointer.
FIGURE 10.7. Dangerous polymorphism: Calling a virtual Reorder function through a DatedStockItem pointer to a DatedStockItem object
Again, the net result of the call illustrated in Figure 10.7 is the same as that illustrated in Figure 9.37: DatedStockItem::Reorder is called. This is correct in this situation. Finally, Figure 10.8 shows a virtual call for a DatedStockItem object through a StockItem pointer.
FIGURE 10.8. Dangerous polymorphism: Calling a virtual Reorder function through a StockItem pointer to a DatedStockItem object
Figure 10.8 is where the virtual function pays off. The correct function, DatedStockItem::Reorder, is called even though the type of the pointer through which it is called is StockItem*. This is in contrast to the result of that same call with the non-virtual function, illustrated in Figure 9.38. In that case, StockItem::Reorder rather than DatedStockItem::Reorder was called.
Susan had a question about those last few example programs:
Susan: I didn't see where you ever deleted the memory for those pointers. Wouldn't that cause a memory leak?
Steve: Oops, you're right. That's a good example of how easy it is to misuse dynamic memory allocation!
What happens if we add another virtual function, Write, for instance, to the StockItem class after the Reorder function? The new virtual function will be added to the vtables for both the StockItem and DatedStockItem classes. Then the situation for a StockItem object might look like Figure 10.9, and the situation for a DatedStockItem might look like Figure 10.10.
As you can see, the new function has been added to both vtables, so a call to Write through a base class pointer will call the correct function.
To translate this virtual function mechanism into what I hope is understandable English, we can express the call to the virtual function Write in the line SIPtr->Write(cout); as follows:
1. Get the vtable address from the object whose address is in SIPtr.
2. Since we are calling Write through a StockItem*, and Write is the second defined virtual function in the StockItem class, retrieve the address of the Write function from the second function address slot in the vtable for the actual object that the StockItem* points to.
3. Execute the function at that address.
FIGURE 10.9. Dangerous polymorphism: A simplified StockItem object with two virtual functions
By following this sequence, you can see that while both versions of Write are referred to via the same relative position in both the StockItem and the DatedStockItem vtables, the particular version of Write that is executed depends on which vtable the object refers to. Since all objects of the same class have the same member functions, all StockItem objects point to the same StockItem vtable and all DatedStockItem objects point to the same DatedStockItem vtable.
FIGURE 10.10. Dangerous polymorphism: A simplified DatedStockItem with two virtual functions
Susan had some questions about adding a new virtual function:
Susan: What do you mean by "added to both vtables"? Do StockItem and DatedStockItem each have their own?
Steve: Yes.
Susan: How does the vtable get the address for the new StockItems?
Steve: It's the other way around. Each StockItem, when it's created by the constructor, has its vtable address filled in by the compiler automatically.

Problems with Using Pointers for Polymorphism

Unfortunately, it's not quite as simple to make polymorphism work for us as this might suggest. As is so often the case, the culprit is the use of pointers. To see how pointers cause trouble with polymorphism, let's start by adding the standard I/O functions, operator << and operator >>, to our simplified interface for the StockItem and DatedStockItem classes. Figure 10.11 shows a test program illustrating how we can use these new functions, Figure 10.12 shows the output of the test program, and Figure 10.13 shows the new version of the interface. I strongly recommend that you print out that header file and the test program for reference as you leaf through this section of the chapter; the latter file is polyioa.cpp.
FIGURE 10.11. Dangerous polymorphism: Using operator << with a StockItem* (code\polyioa.cpp)
#include <iostream>
#include "Vec.h"
#include "itemc.h"
using namespace std;

int main()
{
Vec <StockItem*> x(2);

x[0] = new StockItem("3-ounce cups",71,78);

x[1] = new DatedStockItem("milk",76,87,"19970719");

cout << "A StockItem: " << endl;
cout << x[0] << endl;

cout << "A DatedStockItem: " << endl;
cout << x[1] << endl;

delete x[0];
delete x[1];

return 0;
}

FIGURE 10.12. Result of using operator << with a StockItem* (code\polyioa.out)
A StockItem:
0
3-ounce cups
71
78

A DatedStockItem:
19970719
milk
76
87

FIGURE 10.13. Dangerous polymorphism: StockItem interface with operator << and operator >> (code\itemc.h)
class StockItem
{
friend std::ostream& operator << (std::ostream& os, StockItem* Item);
friend std::istream& operator >> (std::istream& is, StockItem*& Item);

public:
StockItem(std::string Name, short InStock, short MinimumStock);
virtual ~StockItem();

virtual void Reorder(std::ostream& os);
virtual void Write(std::ostream& os);

protected:
std::string m_Name;
short m_InStock;
short m_MinimumStock;
};

class DatedStockItem: public StockItem
{
public:
DatedStockItem(std::string Name, short InStock,
short MinimumStock, std::string Expires);

virtual void Reorder(std::ostream& os);
virtual void Write(std::ostream& os);

protected:
static std::string Today();

protected:
std::string m_Expires;
};

Susan had some questions about the StockItem::~StockItem destructor declared in this latest version of the interface.
Susan: Why do we need a destructor for StockItem now, when we didn't need one before?
Steve: The reason we haven't needed a destructor for the StockItem class until now is that the compiler-generated destructor works fine as long as two conditions are present. First, the member variables of the class must all be of concrete data types (which they are here). Second, the class must have no virtual functions, which of course isn't true for StockItem anymore. We've discussed the reason for the first condition: if we have member variables that are not of concrete data types (e.g., pointers), they won't clean up after themselves properly. We'll find out exactly why the second condition is important as soon as we get through looking at the output of the sample program.
Susan: Okay, I'm sure I can wait. But why is the destructor virtual?
Steve: We'll cover that at the same time.
The first item of note in the test program in Figure 10.11 is that we can create a Vec of StockItem*s to hold the addresses of any mixture of StockItems and DatedStockItems, because we can assign the addresses of variables of either of those types to a base class pointer (i.e., a StockItem*). Once we have the Vec of StockItem*s, we use operator new to acquire the memory for whichever type of object we're creating. This allows us to access these objects via pointers rather than directly and thus to use polymorphism. Once we finish using the objects, we have to make sure they are properly disposed of by calling operator delete at the end of the program; otherwise, a memory leak results.
The calls to delete in Figure 10.11 also hold the key to Susan's question about why we needed to write a destructor for this new version of the StockItem class. You see, when we call operator delete for an object of a class type, delete calls the destructor for that object to do whatever cleanup is necessary at the end of the object's lifespan. For this reason, it is very important that the correct destructor is called. If a base class destructor were called instead of a derived class destructor, the cleanup of the fields defined in the derived class wouldn't occur. However, when we delete a derived class object through a base class pointer, as we are doing in the current example program, the compiler can't tell at compile time which destructor it should call when the program executes. What do we do when we need to delay the determination of the exact version of a function until run time? We use a virtual function. Therefore, whenever we want to call delete on an object through a base class pointer, we need to make the destructor for that object virtual.4
But that still doesn't explain exactly why we need a virtual destructor whenever we have any other