Sharing a Worker Object

Suppose that we have the example program in Figure 10.33 (which is contained in the file refcnt1.cpp if you want to print it out).

Figure 10.33. Safe polymorphism: An example program for reference-counting with StockItems (code efcnt1.cpp)
#include <iostream>
#include <string>
#include "itemp.h"
using namespace std;
int main()
{
   StockItem item1("cups",32,129,10,5,"Bob's Dist.",
     "2895657951"); // create an undated object

   StockItem item2("Hot Chicken",48,158,15,12,"Joe's Dist.",
     "987654321", "19960824"); // create a dated object

    StockItem item3 = item1; // copy constructor
    item1 = item2; // assignment operator

    item1.FormattedDisplay(cout); // display an object with labels

    return 0;
}

This program doesn't do anything useful, except to illustrate the constructors and assignment operator via the following steps:

1.
First, it creates two StockItems, item1 and item2 via the “normal” constructors for undated and dated items, respectively.

2.
Then it creates another StockItem, item3, with the same value as that of item1, via the copy constructor.

3.
Then it assigns the value of item2 to item1 via operator =.

4.
Finally, it displays the resulting value of item1.

The statement StockItem item1("cups", 32, 129, 10, 5, "Bob's Dist.", "2895657951"); calls the constructor illustrated in Figure 10.34 to create a StockItem whose m_Worker points to an UndatedStockItem, because the arguments "cups", 32, 129, 10, 5, "Bob's Dist.", and "2895657951" match the argument list for that constructor.

Figure 10.34. Safe polymorphism: A normal constructor to create a StockItem without a date (from codeitemp.cpp)
StockItem::StockItem(string Name, short InStock,
  short Price, short MinimumStock, short MinimumReorder,
  string Distributor, string UPC)
: m_Count(0),
  m_Worker(new UndatedStockItem(Name, InStock, Price,
  MinimumStock, MinimumReorder, Distributor, UPC))
{
  m_Worker->m_Count = 1;
}

Susan had some comments about this normal constructor for the new version of the StockItem class as well as about the other normal constructor (in Figure 10.36 on page 723):

Susan: I don't get how this can be a normal constructor if it has m_Worker in it. The normal ones are the ones you wrote before this. I am confused. Same thing with Figure 10.36; these are not normal constructors for StockItem.

Steve: The implementation of the constructor doesn't determine whether it is “normal”. A “normal” constructor is one that creates an object with specified data in it, as opposed to a default constructor (Figure 10.31 on page 715) or a copy constructor (Figure 10.38 on page 724).

Immediately after the execution of the constructor call StockItem item1("cups", 32, 129, 10, 5, "Bob's Dist.", "2895657951");, the newly constructed StockItem object and its UndatedStockItem worker object look something like the diagram in Figure 10.35.[9]

[9] In this and the following diagrams, I've omitted a number of the data elements to make the figure fit on the page.

Figure 10.35. Safe polymorphism: A polymorphic StockItem object with an UndatedStockItem worker


The next line in Figure 10.33 on page 720, StockItem item2("Hot Chicken", 48, 15, 12, "Joe's Dist.", "987654321", "19960824");, calls the constructor shown in Figure 10.36, which creates a StockItem whose m_Worker member variable points to a DatedStockItem. As you can see, this is almost identical to the previous constructor, except of course that it creates a DatedStockItem as the worker object rather than an UndatedStockItem.

After the statement StockItem item2("Hot Chicken", 48, 158, 15, 12, "Joe's Dist.", "987654321", "19960824"); is executed, the newly constructed StockItem object and its DatedStockItem worker object looks something like the diagram in Figure 10.37.

Figure 10.36. Safe polymorphism: A normal constructor that constructs a StockItem having a date (from codeitemp.cpp)
StockItem::StockItem(string Name, short InStock,
  short Price, short MinimumStock, short MinimumReorder,
  string Distributor, string UPC, string Expires)
: m_Count(0),
  m_Worker(new DatedStockItem(Name, InStock, Price,
  MinimumStock, MinimumReorder, Distributor, UPC, Expires))
{
  m_Worker->m_Count = 1;
}

Figure 10.37. Safe polymorphism: A polymorphic StockItem object with a DatedStockItem worker


Now let's take a look at what happens when we execute the next statement in Figure 10.33 on page 720, StockItem item3 = item1;. Since we are creating a new StockItem with the same contents as an existing StockItem, this statement calls the copy constructor, which is shown in Figure 10.38.

Figure 10.38. Safe polymorphism: The copy constructor for StockItem (from codeitemp.cpp)
StockItem::StockItem(const StockItem& Item)
: m_Count(0), m_Worker(Item.m_Worker)
{
  m_Worker->m_Count ++;
}

Susan wanted to know why we would want to create a new StockItem with the same contents as an existing one.

Susan: Why would you want to create a new StockItem just like an old one? Why not just use the old one again?

Steve: The most common use for the copy constructor is when we pass an argument by value or return a value from a function. In either of these cases, the copy constructor is used to make a new object with the same contents as an existing one.

Susan: I still don't see why we need to make copies rather than using the original objects.

Steve: In the case of a value argument, this is necessary because we don't want to change the value of the caller's variable if we change the local variable.

In the case of a return value, it's necessary to make a copy because an object that is created inside the called function will cease to exist at the end of the function, before the calling function can use it. Therefore, when we return an object by value we are actually asking the compiler to make a copy of the object; the copy is guaranteed to last long enough for the calling function to use it.

This copy constructor uses the pointer from the existing StockItem object (Item.m_Worker) to initialize the newly created StockItem object's pointer, m_Worker, so that the new and existing StockItem objects will share a worker object. Then we initialize the value of m_Count in the new object to 0 so that it has a known value. Finally, we increment the m_Count variable in the worker object because it has one more user than it had before.

After this operation, the variables item1 and item3, together with their shared worker object, look something like Figure 10.39.

Figure 10.39. Safe polymorphism: Two polymorphic StockItem objects sharing the same UndatedStockItem worker object


Why is it all right to share data in this situation? Because we have the m_Count variable to keep track of the number of users of the worker object so that our StockItem destructor will know when it is time to delete that object. This should be abundantly clear when we get to the end of the program and see what happens when the destructor is called for the StockItem variables.

Now let's continue by looking at the next statement in Figure 10.33 on page 720: item1 = item2;. As you may have figured out already, this is actually a call to the assignment operator (operator =) for the StockItem class.

In case that wasn't obvious to you, don't despair; it wasn't to Susan either:

Susan: No, I didn't figure out that this is a call to the assignment operator. It seems to me that you are setting item2 to item1. I see that operator = has to be called to do this, but I don't see how the program is intentionally calling it.

Steve: Whenever we assign the value of one object to another existing object of the same class, we are calling the assignment operator of that class. That's what “=” means for objects.

The code for that operator is shown in Figure 10.40.

Figure 10.40. Safe polymorphism: The assignment operator (operator =) for StockItem (from codeitemp.cpp)
StockItem& StockItem::operator = (const StockItem& Item)
{
  StockItem* temp = m_Worker;

  m_Worker = Item.m_Worker;
  m_Worker->m_Count ++;

  temp->m_Count --;
  if (temp->m_Count <= 0)
    delete temp;

  return *this;
}

This function starts out with the line StockItem* temp = m_Worker, which makes a copy of the pointer to the worker object that we are currently using. We'll see why we have to do this shortly.

The next statement in the code for operator =, m_Worker = Item.m_Worker;, copies the worker object pointer from the object we are copying from to our object. Now item1 and item2 are sharing a worker object, so they are effectively the same. This also means that worker object has one more manager, so we have to increase that worker object's manager count (in this case to 2), which we do in the next line, m_Worker->m_Count++.

Now we have to correct the count of managers for our previous worker object, which is why we saved its address in the variable temp. Of course, that worker object now has one less manager object. Therefore, we have to execute the statement temp->m_Count --; to correct that count.

If you look at Figure 10.39, you'll see that the previous value of that variable was 2, so it is now 1, meaning that there is one StockItem that is still using that worker object. Therefore, the condition in the next line, if (temp->m_Count <= 0), is false, which means that the controlled statement of the if statement — delete temp; — is not executed. This is correct because as long as there is at least one user of the worker object, it cannot be deleted.

Finally, as is standard with assignment operators, we return to the calling function by the statement return *this;, which returns the object to which we have assigned a new value.

When all this has been done, item1 and item2 will share a DatedStockItem, while item3 will have its own UndatedStockItem. The manager variables item1 and item2, together with their shared worker object, look something like Figure 10.41, and item3 looks pretty much as shown in Figure 10.42.

Figure 10.41. Safe polymorphism: Two polymorphic StockItem objects sharing the same DatedStockItem worker object


Figure 10.42. Safe polymorphism: A polymorphic StockItem object


You should note that the assignment statement item1 = item2; has effectively changed the type of item1 from UndatedStockItem to DatedStockItem. This is one of the benefits of using polymorphic objects; the effective type of an object can vary not only when it is created, but at any time thereafter. Therefore, we don't have to be locked in to a particular type when we create an object, but can adjust the type as necessary according to circumstances. By the way, this ability to change the effective type of an object at run time also solves the slicing problem referred to in Chapter 9, where we assigned an object of a derived class to a base class object with the result that the extra fields from the derived class object were lost.

Susan and I had quite a discussion about the implementation of this version of operator =:

Susan: If this is an assignment operator, then how come it has the code for reference counting in it?

Steve: It needs code for reference counting because it has to keep track of the number of users of its former worker object and its new worker object. First, it has to make a copy of the pointer to its worker object, so it can adjust the number of managers for that worker. The following line does that:

StockItem* temp = m_Worker;

Next, it has to copy the pointer to the worker object from “Item”, like so:

m_Worker = Item.m_Worker;

Susan: Wait a minute. I don't understand what this part does.

Steve: It makes the current object (the one pointed to by this) share a “worker” object with the manager object on the right of the =, which we refer to here as Item.

Susan: Okay.

Steve: Next, it has to increment the number of users of the worker object from Item, since that worker object is now being used by “our” object (i.e., the one pointed to by this). That's taken care of by the line:

m_Worker->m_Count ++;

Next, we take care of adjusting the manager count for our former worker object, which is handled by the line:

temp->m_Count --;

If there aren't any more users of that worker object, then it can (and indeed must) be deleted. That's the purpose of the lines:

if (temp->m_Count <= 0)
   delete temp;

Susan: So, do we have pointers pointing to pointers here?

Steve: Not exactly: We have one object containing a pointer to another object.

The Order of Destructor Calls

Now let's take a look at what happens when the StockItem objects are automatically destroyed at the end of the main program. As the C++ language specifies for the destruction of auto variables, the last to be created will be the first to be destroyed. Thus, item3 will be destroyed first, followed by item2, and finally item1. Figure 10.43 shows the code for the destructor for StockItem.

Figure 10.43. Safe polymorphism: The destructor for the StockItem class (from codeitemp.cpp)
StockItem::~StockItem()
{
  if (m_Worker == 0)
    return;

  m_Worker->m_Count --;
  if (m_Worker->m_Count <= 0)
    delete m_Worker;
}

Before we get into the details of this code, I should mention that whenever any object is destroyed, all of its constituent elements that have destructors are automatically destroyed as well. In the case of a StockItem, the string variables are automatically destroyed during the destruction of the StockItem.

Susan had some questions about the destructor. Here's the first installment:

Susan: Why are you doing reference counting again in the destructor? Is that where it belongs? So, you have to reference-count twice, once for when something is added and again, with different code for when something is subtracted?

Steve: Close; actually, it's a bit more general. Every time a worker object acquires another manager, we have to increment the worker's reference count, and every time it loses one of its managers, we have to decrement the worker's reference count. That way, when the count gets to 0, we know there aren't any more managers for that worker, and we can therefore use delete to destroy the worker object.

Actually, it should be easy to remember this by analogy with a real employment situation: if you're a worker and you no longer have any managers, you're likely to be laid off pretty soon!

Susan: So with this reference counting, the whole point is to know when to use delete?

Steve: Yes.

Susan: How does the delete operator automatically call the destructor for that variable?

Steve: The compiler does that for you. Calling delete for a variable that has a destructor always calls the destructor for that variable.

The first if statement in the destructor, if (m_Worker == 0), provides a clue as to why our special StockItem constructor (Figure 10.32 on page 717) had to set the m_Worker variable to 0. We'll see exactly how that comes into play shortly. For now, we know that the value of m_Worker in item3 is not 0, as it points to an UndatedStockItem (see Figure 10.42 on page 731), so the condition in the first if statement is false. So we move to the next statement, m_Worker->m_Count--;. Since, according to Figure 10.42, the value of m_Count in the UndatedStockItem to which m_Worker points is 1, this statement reduces the value of that variable to 0, making the condition in the statement if (m_Worker->m_Count <= 0) true. This means that the controlled statement of the if statement, delete m_Worker;, is executed. Since the value of m_Count is 0, no other StockItem variables are currently using the UndatedStockItem pointed to by m_Worker. Therefore, we want that UndatedStockItem to go away so that its memory can be reclaimed. We use the delete operator to accomplish that goal.

Susan had a question about why I used <= as the condition in the if statement.

Susan: Why did you say <= 0? How could the count ever be less than 0?

Steve: That's a very good question. If the program is working correctly, it can't. This is a case of “defensive programming”: I wanted to make sure that if, by some error, the count got below 0, the program wouldn't hang onto the memory for the worker object forever. In a production program, it would probably be a good idea to record such an “impossible” condition somewhere so the maintenance programmer could take a look at it.

This is not quite as simple as it may seem, however, because before the memory used by an UndatedStockItem can be reclaimed, the destructor for that UndatedStockItem must be called to allow all of its constituent parts (especially the string variables it contains) to expire properly. As I've mentioned, when we call the delete operator for a variable that has a destructor, it automatically calls the destructor for that variable. Therefore, the next function called is the destructor for the UndatedStockItem being deleted.

Before going into the details of this function, I should explain why it is called in the first place. Remember, we're using StockItem*s to refer to either DatedStockItem or UndatedStockItem objects. How does the compiler know to call the right destructor?

Just as it does with other functions in the same situation. We have to make the destructor virtual so that the correct destructor will be called, regardless of the type of the pointer through which the object is accessed. This answers the earlier question of why we had to define a destructor and declare it to be virtual in Figure 10.13. As a general rule, destructors should be virtual if there are any other virtual functions in the class, as we have to make sure that the right destructor will be called via a base class pointer. Otherwise, data elements that are defined in the derived class won't be destroyed properly, possibly resulting in memory leaks or other undesirable effects.

At this point, Susan had some questions on virtual destructors and the nature of reality (as it applies to C++, at least):

Susan: How are destructors virtual? Do they have a vtable?

Steve: They're in the vtable if they're declared to be virtual, as the one in the final version of StockItem is.

Susan: With all this virtual stuff going on, what is really real? What is the driving force behind all this? It seems like this is a fun house of smoke and mirrors, and I can't tell any more what is really in control.

Steve: The StockItem object is the “manager”, who takes credit for work done by a “worker” object; the worker is either a DatedStockItem or an UndatedStockItem. Hopefully, the rest of the discussion will clarify this.

We don't have to write a destructor for UndatedStockItem because the compiler-generated one does the job for us. But what exactly does that compiler-generated destructor do?

It calls the destructor for every member variable in the class that has a destructor. This is necessary to make sure that those member variables are properly cleaned up after their scope expires when the object they're in goes away. In addition, just as the constructor for a derived class always calls a constructor for its embedded base class object, so a destructor for a derived class always calls the destructor for the base class part of the derived class object. There are two differences between these situations, however:

  1. The constructor for the base class part of the object is called before any of the code in the derived class constructor is executed; the base class destructor is called after the code in the derived class destructor is executed. Here, of course, this distinction is irrelevant because we haven't written any code in the derived class destructor.

  2. There is only one destructor for a given class.

This second difference between constructors and destructors means that we can't use a trick similar to the “special base class constructor” that we used to prevent the base class constructor from calling the derived class constructor again. Instead, we have to arrange a way for the base class destructor to determine whether it's being asked to destroy a “real” base class object (a StockItem) or the embedded base class part of a derived class object (the StockItem base class part of an UndatedStockItem or DatedStockItem). In the latter case, the destructor should exit immediately, since the StockItem base class part of either of those classes contains nothing that needs special handling by the destructor. The special constructor called by the UndatedStockItem constructor to initialize its StockItem part (Figure 10.32 on page 717) works with the first if statement in the StockItem destructor (Figure 10.43 on page 731) to solve this problem. The special StockItem constructor sets m_Worker to 0 (which cannot be the address of any object) during the initialization of the base class part of an UndatedStockItem. When the destructor for StockItem is executed, a 0 value for m_Worker is the indicator of a StockItem that is the base class part of a derived class object. This allows the StockItem destructor to distinguish between a real StockItem and the base class part of an object of a class derived from StockItem by examining the value of m_Worker and bailing out immediately if it is the reserved value of 0.

Tracing the Destruction of Our Polymorphic Objects

Now it's time to go into detail about what happens when item3, item2, and item1 are destroyed. They go away in that order because the last object to be constructed on the stack in a given scope is the first to be destroyed, as you might expect when dealing with stacks.

Figure 10.44 is another listing of the destructor for the StockItem class (StockItem::~StockItem()), for reference as we trace its execution.

Figure 10.44. Safe polymorphism: The destructor for the StockItem class (from codeitemp.cpp)
StockItem::~StockItem()
{
 if (m_Worker == 0)
   return;

 m_Worker->m_Count --;
 if (m_Worker->m_Count <= 0)
   delete m_Worker;
}

At the end of the main program (Figure 10.42 on page 731), item3, which was the last StockItem to be created, is the first to go out of scope. At that point, the StockItem destructor is automatically invoked to clean up. Since the value of m_Worker in item3 isn't 0, the statement controlled by the first if statement isn't executed. Next, we execute the statement m_Worker->m_Count--;, which reduces the value of the variable m_Count in the UndatedStockItem pointed to by m_Worker to 0. Since this makes that variable 0, the condition in the second if statement is true, so its controlled statement, delete m_Worker;, is executed. As we've seen, this eliminates the object pointed to by m_Worker, calling the UndatedStockItem destructor in the process.

As before, calling this destructor does nothing other than destroy the member variable that has a destructor (namely, the m_Expires member variable, which is a string), followed by the mandatory call to the base class destructor.

At that point, the first if statement in StockItem::~StockItem comes into play along with its controlled statement. These two statements are

if (m_Worker == 0)
  return;

Remember the special base class constructor StockItem(int) (Figure 10.32 on page 717)? That constructor, which is called from our UndatedStockItem default and normal constructors, initializes m_Worker to 0. Thus, we know that m_Worker will be 0 for any object that is actually an UndatedStockItem or a DatedStockItem (as distinct from one that is actually a StockItem), because all of the constructors for DatedStockItem call one of those two constructors for UndatedStockItem to initialize their UndatedStockItem base class part. Therefore, this if statement will be true for the base class part of all UndatedStockItem and DatedStockItem objects. Since the current object being destroyed is in fact an UndatedStockItem object, the if is true, and so the destructor exits immediately, ending the destruction of the UndatedStockItem object. Then the destructor for item3 finishes by freeing the storage associated with that object.

Next, the StockItem destructor is called for item2 (Figure 10.41 on page 729). Since m_Worker is not 0, the condition in the first if in Figure 10.44 on page 736 is false, so we proceed to the next statement, m_Worker->m_Count--;. As you can see by looking back at Figure 10.41, the previous value of that variable was 2, so it is now 1. As a result, the condition in the next if statement, if (m_Worker->m_Count <= 0), is also false. Thus, the controlled statement that uses delete to get rid of the DatedStockItem pointed to by m_Worker is not executed. Then the destructor for item2 finishes by freeing the storage associated with that object.

Finally, item1 dies at the end of its scope. When it does, the StockItem destructor is called to clean up after it. As before, m_Worker isn't 0, so the controlled statement of the first if statement in Figure 10.44 on page 736 isn't executed. Next, we execute the statement m_Worker->m_Count--;, which reduces the value of the variable m_Count in the DatedStockItem pointed to by m_Worker to 0. This time, the condition in the next if statement is true, so its controlled statement, delete m_Worker;, is executed. We've seen that this eliminates the object pointed to by m_Worker, calling the appropriate destructor for the worker object, which in this case is DatedStockItem::~DatedStockItem. As with the UndatedStockItem destructor, the compiler-generated destructor for DatedStockItem calls all the destructors for the member variables and base class part, which in this case is an UndatedStockItem. Therefore, we don't have to write this destructor ourselves.

Lastly, the call to the StockItem destructor occurs exactly as it did in the destruction of item3, except that there is an additional step because the base class of DatedStockItem is UndatedStockItem, and the destructor for that class in turn calls the destructor for its base class part, which is a StockItem. Once we get to the destructor for StockItem, the value of m_Worker is 0, so that destructor simply returns to the destructor for UndatedStockItem, which returns to the destructor for DatedStockItem. Then the destructor for item1 finishes by freeing the storage associated with that object.

If you find the above explanation clear, congratulations. Susan didn't:

Susan: How do you know that the m_Worker can never be 0 in a real StockItem and is always 0 in a StockItem that is a base class part of a derived class object?

Steve: Because I set m_Worker to 0 in the special StockItem constructor called from the base class initializer in the default and normal constructors for UndatedStockItem. Therefore, that special constructor is always used to set up the base class part of an UndatedStockItem, and since DatedStockItem derives from UndatedStockItem, the same will be true of a DatedStockItem.

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

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