Review

We started this chapter with the DatedStockItem class from Chapter 9, which extended the StockItem class by adding an expiration date field to the member variables that DatedStockItem inherited from the StockItem class. While this was a solution to the problem of creating a class based on the StockItem class without having to rewrite all the functioning code in the latter class, it didn't solve the bigger problem: how to create a Vec of objects that might or might not have expiration dates. That is, we wanted to be able to mix StockItem objects with DatedStockItem objects in the same Vec, which can't be done directly in C++.

Part of this difficulty was solved easily enough by making a Vec of StockItem*s (i.e., pointers to StockItems), rather than StockItems, to take advantage of the fact that C++ allows us to assign the address of an object of a derived type to a pointer to its base class (e.g., to assign the address of a DatedStockItem to a pointer of type StockItem*). Creating such a Vec of StockItem*s allowed us to create both StockItems and DatedStockItems and to assign the addresses of these objects to various elements of that Vec. Even so, this didn't solve the problem completely. When we called the Reorder function through a StockItem pointer, the function that was executed was always StockItem::Reorder. This result may seem reasonable, but it doesn't meet our needs. When we call the Reorder function for an object, we want to execute the correct Reorder function for the actual type of the object the pointer is referring to even though the pointer is declared as a StockItem*.

The reason the StockItem function is always called in this situation is that when we make a base class pointer (e.g., a StockItem*) refer to a derived class object (e.g., a DatedStockItem), the compiler doesn't know what the actual type of the object is at compile time. By default, the compiler determines exactly which function will be called at compile time, and the only information the compiler has about the type of an object pointed to by a StockItem* is that it's either a StockItem or an object of a class derived from StockItem. In this situation, the compiler defaults to the base class function.

The solution to this problem is to make the Reorder function virtual. This means that when the compiler sees a call to the Reorder function, it generates code that will call the appropriate version of that function for the actual type of the object referred to. In this case, StockItem::Reorder is called if the actual object being referred to through a StockItem* is a StockItem, while DatedStockItem::Reorder is called if the actual object being referred to through a StockItem* is a DatedStockItem. This is exactly the behavior we need to make our StockItem and DatedStockItem objects do the right thing when we refer to them through a StockItem*.

To make this run-time determination of which function will be called, the compiler has to add something to every object that contains at least one virtual function. What it adds is a pointer to a vtable (virtual function table), which contains the addresses of all the virtual functions defined in the class of that object or declared in any of its ancestral classes. The code the compiler generates for a virtual function call uses this table to look up the actual address of the function to be called at run time.

After going over the use of virtual functions, we looked at the implementation of the usual I/O functions, operator << and operator >>. Even though these can't be virtual functions, as they aren't member functions at all, they can still use the virtual mechanism indirectly by calling virtual functions that provide the correct behavior for each class. However, while this solves most of the problems with operator <<, the implementation of operator >> is still pretty tricky. It has to allocate the memory for the object it is creating because the actual type of that object isn't known until the data for the object has been read. The real problem here, though, is not that operator >> has to allocate that memory, or even that it has to use a reference to a pointer to notify the calling function of the address of the allocated memory. The big difficulty is that after operator >> allocates the memory, the calling function has to free the memory when it is done with the object. Getting a program written this way to work properly is very difficult; keeping it working properly after later changes is virtually impossible. Unfortunately, there's no solution to this problem as long as we require the user program to use a Vec of StockItem*s to get the benefits of polymorphism.

As is often the case in C++, though, there is a way to remove that requirement. We can hide the pointers from the user, and eliminate the consequent memory allocation problems, by creating a polymorphic object using the manager/worker idiom. With these idioms, the user sees only a base class object, within which the pointers are hidden. The base class is the manager class, which uses an object of one of the worker classes to do the actual work.

Possibly the most unusual aspect of this implementation of the manager/worker idiom is that the type of the pointer inside the manager object is the same as the type of the manager class. In the current case, each StockItem object contains a StockItem* called m_Worker as its main data member. This may seem peculiar, but it makes sense. The actual type of the object being referred to is always one of the worker classes, which in this case means either UndatedStockItem or DatedStockItem. Since we know that a StockItem* can refer to an object of any of the derived classes of StockItem, and because we want the interface of the StockItem class to be the same as that of any of its derived classes, declaring the pointer to the worker object as a StockItem* is quite appropriate.

We started our examination of this new StockItem class with operator <<, whose header indicates one of the advantages of this new implementation of StockItem. Rather than taking a StockItem*, as the previous version of operator << did, it takes a const reference to a StockItem. The implementation of this function consists of a call to a virtual function called Write. Since Write is virtual, the exact function that will be called here depends on the actual type of the object pointed to by m_Worker, which is exactly the behavior we want.

After going into more detail on how this “internal polymorphism” works, we looked at how such a polymorphic object comes into existence in the first place, starting with the default constructor for StockItem. This constructor is a bit more complicated than it seems at first. When it creates an empty UndatedStockItem to perform the duties of a default-constructed StockItem, that newly constructed UndatedStockItem has to initialize its base class part, as is required for all derived class objects. That may not seem too unusual, but keep in mind that the base class for UndatedStockItem is StockItem; therefore, the constructor for UndatedStockItem will necessarily call a constructor for StockItem. We have to make sure that it doesn't call the default constructor for StockItem, as that is where the UndatedStockItem constructor was called from in the first place! The result of such a call would be a further call to the default constructor for UndatedStockItem, then to the default constructor for StockItem, and so on forever (or at least until we had run out of stack space). The solution is simple enough: we have to create a special constructor for StockItem, StockItem::StockItem(int), that we call explicitly via a base class initializer in the default and normal constructors for UndatedStockItem. This special constructor doesn't do anything but initialize m_Worker to 0, for reasons we'll get to later, and then return to its caller. Since it doesn't call any other functions, we avoid the potential disaster of an infinite regress.

This simple function, StockItem::StockItem(int), does have a couple of other interesting features. First, I declared it protected so that it couldn't be called by anyone other than our member functions and those of our derived classes. This is appropriate because, after all, we don't want a user to be able to create a StockItem with just an int argument. In fact, we are using that argument only to allow the compiler to tell which constructor we mean via the function overloading facility. This also means that we don't need to provide a name for that argument, since we're not using it. Leaving the name out tells the compiler that we didn't accidentally forget to use the argument, so we won't get “unused argument” warnings when we compile this code. Any programmers who may have to work on our code in the future will also appreciate this indication that we weren't planning to use the argument.

Then we examined the use of reference-counting to keep track of the number of users of a given DatedStockItem or UndatedStockItem object, rather than copying those objects whenever we copied their manager objects. As long as we keep track of how many users there are, we can safely delete the DatedStockItem or UndatedStockItem as soon as there are no more users left. To keep track of the number of users, we use the m_Count member variable in the StockItem class.

To see how this works in practice, we went through the actions that occur when StockItem objects are created, copied, and destroyed. This involves the implementation of the assignment operator, which copies the pointer to the worker object contained in the manager object. We can do this because we are using reference-counting to keep track of the number of users of each object, so memory is freed correctly when the objects are destroyed at the end of the function where they are created.

The destructor for the StockItem class has a number of new features. First, it is virtual, which is necessary because the derived class object pointed to by the StockItem object must be destroyed when it no longer has any users, but the type of the pointer through which it is accessed is StockItem*. To allow the compiler to call the correct destructor — the one for the derived class object — we must declare the base class destructor virtual. This is the same rule that applies to all other functions that have to be resolved according to the run-time type of the object for which they are called.

Second, the StockItem destructor has to check whether it has been called to destroy the base class part of a derived class object. Just as in the case of the constructor for StockItem, we have to prevent an infinite regress in which the destructor for StockItem calls the destructor for DatedStockItem, which calls the destructor for StockItem, which calls the destructor for DatedStockItem again, and so on. In fact, in this case we can't avoid the first “round trip” because the destructor for DatedStockItem must call the destructor for StockItem; we don't have any control over that. However, the first if statement in the StockItem destructor cuts off the regress right there, as m_Worker will be 0 in the base class part of a derived class object. That's why we initialized m_Worker to 0 in the special constructor for StockItem that was used to construct the base class part of an object of a class derived from StockItem.

We finished by examining the exact sequence of events that occurs when the objects in the example program are destroyed. We also determined the reason that we had to include m_Count in the base class, when it was never used there: the type of the pointer we use to access m_Count is a StockItem*, so there has to be an m_Count variable in a StockItem. If there were no such variable in the StockItem class, the compiler couldn't guarantee that it would exist in the object pointed to by a StockItem pointer and therefore wouldn't let us access it through such a pointer.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset