TOC PREV NEXT INDEX

C++: A Dialog


11.8. Adding the Ability to Edit a Record


Figure 11.29 is the latest, greatest version of the interface for the HomeItem class.
FIGURE 11.29. The latest version of the Homeitem class interface (code\hmit5.h)
// hmit5.h

class HomeItem
{
friend std::ostream& operator << (std::ostream& os,
const HomeItem& Item);

friend std::istream& operator >> (std::istream& is, HomeItem& Item);

public:
HomeItem();
HomeItem(const HomeItem& Item);
HomeItem& operator = (const HomeItem& Item);
virtual ~HomeItem();

// Basic: Art objects, furniture, jewelry, etc.
HomeItem(std::string Name, double PurchasePrice,
long PurchaseDate, std::string Description, std::string Category);

// Music: CDs, LPs, cassettes, etc.
HomeItem(std::string Name, double PurchasePrice,
long PurchaseDate, std::string Description, std::string Category,
std::string Artist, Vec<std::string> Track);

virtual void Write(std::ostream& os);
virtual short FormattedDisplay(std::ostream& os);
virtual std::string GetName();
static HomeItem NewItem();

virtual void Read(std::istream& is);
virtual void Edit();

protected:
HomeItem(int);
virtual HomeItem* CopyData();

protected:
HomeItem* m_Worker;
short m_Count;
};

If you compare this interface with the previous version in Figure 11.20, you'll notice that I've added a few new member functions - namely, Edit, CopyData, and Read. We'll get to Read in due time, but we're going to start with HomeItem::Edit. Unlike a "normal" function in a polymorphic object, where the base class version simply passes the buck to the appropriate worker object, the base class version of Edit (Figure 11.30) is a bit more involved.
FIGURE 11.30. HomeItem::Edit (from code\hmit5.cpp)
void HomeItem::Edit()
{
if (m_Worker->m_Count > 1)
{
m_Worker->m_Count --;
m_Worker = m_Worker->CopyData();
m_Worker->m_Count = 1;
}

m_Worker->Edit();
}

The reason that HomeItem::Edit is different from most of the base class functions is that it has to deal with the aliasing problem: the possibility of altering a shared object, which arises when we use reference counting to share one copy of a worker object among a possibly large number of manager objects. Reference counting is generally much more efficient than copying the worker object whenever we copy the manager object, but it has one drawback: if more than one manager object is pointing to the same worker object, and any of those manager objects changes the contents of "its" worker object, all of the other manager objects will also have "their" worker objects changed without their advice or consent. This can cause chaos in a large system.
Luckily, it's not that difficult to prevent, as the example of HomeItem::Edit shows. This function starts by executing the statement if (m_Worker->m_Count > 1), which checks whether this object has more than one manager. If it has only one, we can change it without causing difficulty for its other manager objects; therefore, we skip the code in the {} and proceed directly to the worker class Edit function. On the other hand, if this worker object does have more than one manager, we have to "unhook" it from its other managers. We do this by executing the three statements in the controlled block of the if statement.
First, the statement m_Worker>m_Count --; subtracts 1 from the count in the current worker object to account for the fact that this manager object is going to use a different worker object. Then the next statement, m_Worker = m_Worker>CopyData();, creates a new worker object with the same data as the previous worker object, and assigns its address to m_Worker so that it is now the current worker object for this manager object. Finally, the statement m_Worker>m_Count = 1; sets the count of managers in this new worker object to 1 so that the reference-counting mechanism will be able to tell when this worker object can be deleted.
After these housekeeping chores are finished, we call the Edit function of the new worker object to update its contents.
Now let's take a look at the CopyData helper function. The first oddity is in its declaration; it's a protected virtual function. The reason that it has to be virtual should be fairly obvious: copying the data for a HomeItem derived class object depends on the exact type of the object, so CopyData has to be virtual. However, that doesn't explain why it is protected.
The explanation is that we don't want users of HomeItem objects to call this function. In fact, the only classes that should be able to use CopyData are those in the implementation of HomeItem. Therefore, we make CopyData protected so that the only functions that can access it are those in HomeItem and its derived classes.
The only remaining question that we have to answer about editing a HomeItem object is how the CopyData function works. Because CopyData is inaccessible to outside functions and is always called for a worker class object within the implementation of HomeItem, the base class version of CopyData should never be called and therefore consists of an exit statement. Let's continue by examining the code for HomeItemBasic::CopyData(), which is shown in Figure 11.31.
FIGURE 11.31. HomeItemBasic::CopyData() (from code\hmit5.cpp)
HomeItem* HomeItemBasic::CopyData()
{
HomeItem* TempItem = new HomeItemBasic(m_Name,
m_PurchasePrice, m_PurchaseDate, m_Description, m_Category);

return TempItem;
}

This isn't a terribly complicated function; it simply creates a new HomeItemBasic object with the same contents as an existing object, then returns that new object. One question you may have is why we don't use the copy constructor to do this; isn't that what copy constructors are for?
Yes, normally we would be able to use the copy constructor to make a copy of an object. In this case, however, that won't work, because the copy constructor for these classes uses reference counting to share a single worker object among several manager objects. Of course, what we're doing here is trying to make a new object that isn't shared, so the copy constructor would not do what we needed.
HomeItemMusic::CopyData is just like HomeItemBasic::CopyData except for the type of the new object being created, so I won't bother explaining it separately.

The New Implementation of operator >>

Now that we've cleared up the potential problem with changing the value of a shared worker object, we can proceed to the new version of operator >> (Figure 11.32), which uses Read to fill in the data in an empty HomeItem.
FIGURE 11.32. The latest version of operator >> (from code\hmit5.cpp)
istream& operator >> (istream& is, HomeItem& Item)
{
string Type;
bool Interactive = (&is == &cin);

while (Type == "")
{
if (Interactive)
cout << "Type (Basic, Music) ";
getline(is,Type);
if (is.fail() != 0)
{
Item = HomeItem();
return is;
}
}

if (Type == "Basic")
{
// create empty Basic object to be filled in
HomeItem Temp("",0.0,0,"","");
Temp.Read(is);
Item = Temp;
}
else if (Type == "Music")
{
// create an empty Music object to be filled in
HomeItem Temp("",0.0,0,"","","",Vec<string>(0));
Temp.Read(is);
Item = Temp;
}
else
{
cerr << "Can't create object of type " << Type << endl;
exit(1);
}

return is;
}

The first part of this function, where we determine the type of the object to be created, is just as it was in the previous version (Figure 11.22). However, once we figure out the type, everything changes. Rather than read the data directly from the file or the user, we create an empty object of the correct type and then call a function called Read to get the data for us.
Susan had some questions about the constructor calls that create the empty HomeItemBasic and HomeItemMusic objects.
Susan: Why do you have a period in the middle of one of the numbers when you're making a HomeItemMusic object?
Steve: That's the initial value of the price field, which is a floating-point variable, so I've set the value to 0.0 to indicate that.
Susan: What's a floating-point variable?
Steve: One that can hold a number that has a fractional part as well as a number that has only a whole part.
Susan: Okay, but why do you need all those null things (0 and "") in the constructor calls?
Steve: Because the compiler needs the arguments to be able to figure out which constructor we want it to call. If we just said HomeItem Temp();, we would get a default-constructed HomeItem object that would have a HomeItemBasic worker object, but we want to specify whether the worker object is actually a HomeItemBasic or a HomeItemMusic. If the arguments match the argument list of the constructor that makes a HomeItem manager object with a HomeItemBasic worker object, then that's what the compiler will do; if they match the argument list of the constructor that makes a HomeItemMusic, it will make a HomeItem manager object with a HomeItemMusic worker object instead. That's how we make sure that we get the right type of empty object for the Read function to fill in.
One question not answered in this dialogue is what was wrong with the old method of filling in the fields in the object being created. That's the topic of the next section.

Reducing Code Duplication

The old method of creating and initializing the object directly in the operator >> code was fine for entering and displaying items, but as soon as we want to edit them, it has one serious drawback: the knowledge of field names has to be duplicated in a number of places. As we saw in the discussion of our recent changes to operator >>, this is undesirable because it harms maintainability. For example, let's suppose we want to change the prompt "Name: " to "Item Name: ". If this were a large program, it would be a significant problem to find and change all the occurrences of that prompt. It would be much better to be able to change that prompt in one place and have the whole program use the new prompt, as the new version of the program will allow us to do.
Susan had a question about changing prompts.
Susan: Why would you want to change the prompts? Who cares if it says "Name" or "Item Name"?
Steve: Well, the users of the program might care. Also, what if we wanted to translate this program into another language, like Spanish? In that case, it would be a lot more convenient if all of the prompts were in one place so we could change them all at once.
Before we get into the implementation of Read, however, we should look at the new version of the interface for the worker classes of HomeItem (Figure 11.33), which contains some new member functions as well as some constructs we haven't seen before.
FIGURE 11.33. The latest version of the interface for the HomeItem worker classes (code\hmiti5.h)
// hmiti5.h

class HomeItemBasic : public HomeItem
{

public:
HomeItemBasic();

HomeItemBasic(std::string Name, double PurchasePrice,
long PurchaseDate, std::string Description, std::string Category);

virtual std::string GetName();
virtual void Read(std::istream& is);
virtual void Edit();

virtual void Write(std::ostream& os);
virtual std::string GetType();
virtual short FormattedDisplay(std::ostream& os);

virtual short ReadInteractive();
virtual short ReadFromFile(std::istream &is);
virtual bool EditField(short FieldNumber);

protected:
enum FieldNum {e_Name = 1, e_PurchasePrice,
e_PurchaseDate, e_Description, e_Category};

std::string GetFieldName(short FieldNumber);
virtual HomeItem* CopyData();

protected:
std::string m_Name;
double m_PurchasePrice;
long m_PurchaseDate;
std::string m_Description;
std::string m_Category;
};

class HomeItemMusic : public HomeItemBasic
{
public:
HomeItemMusic(std::string Name, double PurchasePrice,
long PurchaseDate, std::string Description, std::string Category,
std::string Artist, Vec<std::string> Track);

virtual void Write(std::ostream& os);
virtual std::string GetType();
virtual short FormattedDisplay(std::ostream& os);

virtual short ReadInteractive();
virtual short ReadFromFile(std::istream &is);
virtual bool EditField(short FieldNumber);

protected:
enum FieldNum {e_Artist = HomeItemBasic::e_Category + 1,
e_TrackCount, e_TrackNumber};

std::string GetFieldName(short FieldNumber);
virtual HomeItem* CopyData();

protected:
std::string m_Artist;
Vec<std::string> m_Track;
};

Before we get to the new functions, I should tell you about some details of the declaration and implementation of the concrete data type functions in this version of the HomeItem classes. As in previous header files for the worker classes of a polymorphic object, we don't have to declare the copy constructor, operator =, or the destructor for the first derived class, HomeItemBasic. Even so, we do have to declare and write the default constructor for this class so that we can specify the special base class constructor. This is necessary to avoid an infinite regress during the construction of a manager object. However, we don't have to declare any of those functions or the default constructor for the second derived class, HomeItemMusic.
Another thing I should mention is that the functions ReadInteractive, ReadFromFile, and EditField are defined in HomeItemBasic and HomeItemMusic, rather than in HomeItem, because they are used only within the worker class implementations of Read and Edit rather than by the users of these classes. To be specific, the new functions ReadInteractive and ReadFromFile are used in the implementation of Read, and we'll discuss them when we look at Read, whereas the new EditField function is similarly used in the implementation of the Edit function. As in other cases where we've added functions that are not intended for the user of the HomeItem class, I have not defined them in the interface of HomeItem. This is an example of information hiding, similar in principle to making data and functions private or protected. Even though these functions are public, they are defined in classes that are accessible only to the implementers of the HomeItem polymorphic object - us.
There's also a new protected function called GetFieldName defined in HomeItemBasic and HomeItemMusic. It is used to encapsulate the knowledge of the field name prompts in connection with the information stored in the two versions of a list of constant data items. This list, named FieldNum, is a new kind of construct called an enum. Of course, this leads to the obvious question: what's an enum?

The enum Type

An enum is a way to define a number of unchangeable values, which are quite similar to consts. One of the differences between these two types of named values is relevant here: the value of each successive name in an enum is automatically incremented from the value of the previous name (if you don't specify another value explicitly). This is quite convenient for defining names for a set of values such as Vec or array indexes for prompts, which is how we will use enums in our program.
Susan had some questions about enums, starting with the derivation of this keyword.
Susan: What does enum mean? Is it short for something?
Steve: Yes. An enum is called that because it gives names to an "enumeration", which is a list of named items.
Susan: Are you going to put the prompts in a Vec?
Steve: No, but you're close; they'll be in an array, for reasons that I'll explain at the appropriate point.
Let's start with the definition of HomeItemBasic::FieldNum, which is:

enum FieldNum {e_Name = 1, e_PurchasePrice,
e_PurchaseDate, e_Description, e_Category};
This defines an enum called FieldNum that consists of the named values e_Name, e_PurchasePrice, e_PurchaseDate, e_Description, and e_Category. The first of these is defined to have the value 1, and the others have consecutive values starting with 2 and continuing through 5. These values, by absolutely no coincidence, are the field numbers we are going to use to prompt the user for the values of the member variables m_Name, m_PurchasePrice, m_PurchaseDate, m_Description, and m_Category.
The definition of HomeItemMusic::FieldNum is similar, but the values could use some explanation. First, here's the definition:

enum FieldNum {e_Artist = e_Category + 1,
e_TrackCount, e_TrackNumber};
The only significant difference between this definition and the previous one (besides the names of the values) is in the way we set the value of the first data item: we define it as one more than the value of e_Category, which, as it happens, is the last named value in HomeItemBasic::FieldNum. We need to do this so that we can display the correct field numbers for a HomeItemMusic, which of course contains everything that a HomeItemBasic contains as well as its own added variables. The user of the program should be able to edit either of these types of variables without having to worry about which fields are in the derived or the base class part. Thus, we want to make sure that the field number prompts run smoothly from the end of the data entry for a HomeItemBasic to the beginning of the data entry for a HomeItemMusic. If this isn't clear yet, don't worry. It will be by the time we get through the implementation of Read and the other data entry functions.
Now, what about those protected GetFieldName functions? All they do is to return the prompt corresponding to a particular field number. However, they deserve a bit of scrutiny because their implementation isn't quite so obvious as their purpose. Let's start with HomeItemBasic::GetFieldName, which is shown in Figure 11.34.
FIGURE 11.34. HomeItemBasic::GetFieldName (from code\hmit5.cpp)
string HomeItemBasic::GetFieldName(short FieldNumber)
{
static string Name[e_Category+1] = {"","Name",
"Purchase Price","Purchase Date", "Description",
"Category"};

if ((FieldNumber > 0) && (FieldNumber <= e_Category))
return Name[FieldNumber];

return "";
}

This function contains a static array of strings, one for each field prompt and one extra null string at the beginning of the array. To set up the contents of the array, we use the construct {"","Name", "Purchase Price","Purchase Date", "Description", "Category"};, which is an array initialization list that supplies data for the elements in an array.
We need that null string ("") at the beginning of the initialization list to simplify the statement that returns the prompt for a particular field, because the field numbers that we display start at 1 rather than 0 (to avoid confusing the user of the program). Arrays always start at element 0 in C++, so if we want our field numbers to correspond to elements in the array, we need to start the prompts at the second element, which is how I've set it up. As a result, the statement that actually selects the prompt, return Name[FieldNumber], just uses the field number as an index into the array of strings and returns the appropriate one. For example, the prompt for e_Name is going to be "1. Name: ", the prompt for e_PurchasePrice is "2. Purchase Price: ", and so on.
There are two questions I haven't answered yet about this function. First, why is the array static? Because it should be initialized only once, the first time this function is called, and that's what happens with static data. This is much more efficient than reinitializing the array with the same data every time this function is called, which is what would happen if we didn't add the static modifier to the definition of the Name array.
The second question is why we are using an array in the first place - aren't they dangerous? Yes, they are, but, unfortunately, in this situation we cannot use a Vec as we normally would. The reason is that the ability to specify a list of values for an array is built into the C++ language and is not available for user-defined data types such as the Vec. Therefore, if we want to use this very convenient method of initializing a multi-element data structure, we have to use an array instead.
This also explains why we need the if statement: to prevent the possibility of a caller trying to access an element of the array that doesn't exist. With a Vec, we wouldn't have to worry that such an invalid access could cause havoc in the program; instead, we would get an error message from the index-checking code built into Vec. However, arrays don't have any automatic checking for valid indexes, so we have to take care of that detail whenever we use them in a potentially hazardous situation like this one.
There's one more point I should mention here. I've already explained that when we define an enum such as HomeItemBasic::FieldNum, the named values in that enum (such as e_Name) are actually values of a defined data type. To be precise, e_Name is a value of type HomeItemBasic::FieldNum. That's not too weird in itself, but it does lead to some questions when we look at a statement such as if ((FieldNumber > 0) && (FieldNumber <= e_Category)). The problem is that we're comparing a short called FieldNumber with a HomeItemBasic::FieldNum called e_Category. Is this legal, and if so, why?

Automatic Conversion from enum

Yes, it is legal, because an enum value will automatically be converted to an integer value for purposes of arithmetic and comparison. For example, you can compare an enum with a value of any integer type, add an enum to an integer value, or assign an enum to an integer variable, without a peep from the compiler. While this is less than desirable from the point of view of strong type checking, it can be handy in circumstances like the present ones. By the way, this automatic conversion doesn't completely eliminate type checking for enums; you can't assign an integer value to an enum without the compiler warning you that you're doing something questionable, so there is some real difference between enums and integer types!1

The Implementation of HomeItem::Read

Finally, we're ready for the implementation of Read. As with HomeItem::Edit, the base class version of this function has to handle the possibility that we're reading data into a shared worker object, as shown in Figure 11.35.
FIGURE 11.35. HomeItem::Read (from code\hmit5.cpp)
void HomeItem::Read(istream& is)
{
if (m_Worker->m_Count > 1)
{