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 (codehmit5.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 codehmit5.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 codehmit5.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 codehmit5.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 (codehmiti5.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 codehmit5.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![13]

[13] The subject of automatic conversions among the various built-in types in C++ is complex enough to require more coverage than I can provide here. Suffice it to say that it is a minefield of opportunities for subtle errors.

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 codehmit5.cpp)
void HomeItem::Read(istream& is)
{
 if (m_Worker->m_Count > 1)
   {
   m_Worker->m_Count --;
   m_Worker = m_Worker->CopyData();
   m_Worker->m_Count = 1;
   }

 m_Worker->Read(is);
}

I won't bother going over the anti-aliasing code again, as it is identical to the corresponding code in HomeItem::Edit. Instead, let's move right along to Figure 11.36, which shows the worker version of this function, HomeItemBasic::Read. Before reading on, see if you can guess why we need only one worker version of this function rather than one for each worker class.

Figure 11.36. HomeItemBasic::Read (from codehmit5.cpp)
void HomeItemBasic::Read(istream& is)
{
 if (&is == &cin)
   ReadInteractive();
 else
   ReadFromFile(is);
}

The reason we need only one worker class version of this function is that its only job is to decide whether the input is going to be from the keyboard (cin) or from a file, and then to call a function to do the actual work. The class of the worker object doesn't affect the decision as to whether the input is from cin or a file, and the functions it calls are virtual, so the right function will be called for the type of the worker object. This means that the HomeItemMusic version of this function is identical, so we can rely on inheritance from HomeItemBasic to supply it and therefore don't have to write a new version for each derived class.

The Implementation of HomeItemBasic::ReadInteractive

By the same token, the code for Read doesn't tell us much about how reading data for an object actually works; for that we'll have to look at the functions it calls, starting with Figure 11.37, which shows the code for HomeItemBasic::ReadInteractive.

Figure 11.37. HomeItemBasic::ReadInteractive (from codehmit5.cpp)
short HomeItemBasic::ReadInteractive()
{
 short FieldNumber = e_Name;

 cout << FieldNumber << ". ";
 cout << GetFieldName(FieldNumber) << ": ";
 FieldNumber ++;
 getline(cin,m_Name);

 cout << FieldNumber << ". ";
 cout << GetFieldName(FieldNumber) << ": ";
 FieldNumber ++;
 cin >> m_PurchasePrice;

 cout << FieldNumber << ". ";
 cout << GetFieldName(FieldNumber) << ": ";
 FieldNumber ++;
 cin >> m_PurchaseDate;

 cout << FieldNumber << ". ";
 cout << GetFieldName(FieldNumber) << ": ";
 FieldNumber ++;
 getline(cin,m_Description);

 cout << FieldNumber << ". ";
 cout << GetFieldName(FieldNumber) << ": ";
 FieldNumber ++;
 getline(cin,m_Category);

 return FieldNumber;
}

This isn't too complicated, but there are a few tricky parts, starting with the statement short FieldNumber = e_Name;. Why are we using a short value (FieldNumber) to keep track of which field number we are using when we have an enum type called FieldNum that can apparently be used for this purpose?

We have to use a short for this purpose because, as I noted earlier, we can't assign an integer value to an enum without the compiler complaining. Although we aren't directly assigning an integer value to FieldNumber, we are incrementing it by using the ++ operator. This operator, as you may recall, is shorthand for “add one to the previous value of the variable and put the new value back in the variable”. The first part of that operation is allowed with an enum variable because such a variable is automatically converted to an integer type when it is used for arithmetic. However, the second part prevents us from using an enum variable because it tries to assign the integer result of the addition back to the enum, and that violates the rule against assigning an enum an integer value.

Susan had a question about the rules for using enums.

Susan: I understand that we aren't supposed to do arithmetic operations on an enum. What I don't understand is why.

Steve: It's to try to prevent errors in using them. An enum consists of a number of named values. As long as we stick to the rules for using enum values, the compiler can tell whether we're using them correctly. For example, because e_TrackNumber is the highest value defined in the FieldNum enum, if we were to try to refer to the value e_TrackNumber + 1, the compiler could tell that we were doing something illegal. However, if we could add a number to an enum variable, the compiler wouldn't be able to tell if we were doing something illegal because the value of the variable wouldn't be known until run time.

Susan: I still don't get it. I need an example.

Steve: That's a reasonable request. Okay, let's suppose that we used an enum instead of a short and tried to add 1 to it. If we created a local variable of type FieldNum called FieldNumber and tried to add 1 to it via the statement FieldNumber++;, we should get an error message telling us we are trying to convert an enum to an integer type. Unfortunately, the compiler on the CD in the back of the book doesn't seem to comply with the standard in this respect, and will allow such a construct. But we still shouldn't do it.

Susan: Okay, I guess that makes sense. So how do we get around this problem?

Steve: By using a short variable instead of an enum. We can use an enum value to initialize the short variable, then increment the short to keep track of the field number that we're on.

Now that we've presumably cleared up that point, most of the rest of this function is pretty simple; it consists primarily of a number of sequences that are all quite similar. Let's take a look at the first of these sequences, which handles the name of the object.

First, we display the field number for the current field via the statement cout << FieldNumber << ". ";. Next, we retrieve and display the field name for the current field via the next statement, cout << GetFieldName(FieldNumber) << ": ";. Then we increment the field number to set up for the next field via the statement “FieldNumber ++;". Finally, we request the value for the variable corresponding to the name of the object via the last statement in the sequence, getline(cin,m_Name);.

Of course, the sequences that handle the other fields are almost the same as this one, differing only in the name of the variable we're assigning the input value to. However, as simple as this may be, it raises another question: why are we repeating almost the same code a number of times rather than using a function? The problem is that these sequences aren't similar enough to work as a function; to be exact, the type of the variable to which the data is being assigned is different according to which field we're working on. For example, m_Name is a string, m_PurchasePrice is a double, and m_PurchaseDate is a long. Therefore, we would need at least three different functions that were almost identical except for the type of data they returned, which wouldn't be worth the trouble. Instead, we'll just put up with the duplication.

One more point is that I haven't added the usual “ignore” function call that we've needed to use in our previous code that reads a numeric value from the file. Why isn't that necessary here?

Because interactive I/O has special rules: whenever you write something to cout, it clears the buffer that cin uses. Therefore, we don't have to worry about leftover newline characters getting in the way when we read the next line from cin.

The Implementation of HomeItemBasic::ReadFromFile

Now let's take a look at HomeItemBasic::ReadFromFile (Figure 11.38), which, as its name suggests, reads data from a file and uses it to assign a value to a HomeItemBasic object.

Figure 11.38. HomeItemBasic::ReadFromFile (from codehmit5.cpp)
short HomeItemBasic::ReadFromFile(istream& is)
{
 getline(is,m_Name);
 is >> m_PurchasePrice;
 is >> m_PurchaseDate;
 is.ignore();

 getline(is,m_Description);
 getline(is,m_Category);

 return 0;
}

As you can see, this is much simpler than the previous function. However, it does basically the same thing; the difference is merely that it deals with a file rather than a user, which makes its job much easier. This illustrates a maxim known to all professional programmers: having to deal with users is the most difficult part of writing programs!

The Implementation of HomeItemBasic::Edit

We've examined all the functions that make up the implementation of Read for the HomeItemBasic class. Now it's time to take a look at HomeItemBasic::Edit, which is the function called by HomeItem::Edit to edit existing data in a HomeItemBasic worker object. As in the case of Read, this function doesn't do anything class-specific, but hands all the dirty work over to virtual functions that will do the right thing for their objects. Therefore, we need only one version of Edit, which will call the appropriate functions depending on the type of the object we're actually editing. Figure 11.39 shows the code for this function, HomeItemBasic::Edit.

Figure 11.39. HomeItemBasic::Edit (from codehmit5.cpp)
void HomeItemBasic::Edit()
{
 short FieldNumber;

 FormattedDisplay(cout);
 cout << endl;

 cout << "Please enter field number to be changed: ";
 cin >> FieldNumber;
 cin.ignore();
 cout << endl;

 EditField(FieldNumber);
}

There's nothing terribly complicated about this function, largely because it uses FormattedDisplay and EditField to do most of the work. First, it calls FormattedDisplay to display the current value of the object in question, then it asks for the number of the field to be changed, and finally it calls EditField to do the actual modification for that field.

Susan had a question about the field number.

Susan: Is the field number the element number in the Vec of HomeItems?

Steve: No, it's the number of the individual field that we're going to change in the HomeItem that we're editing.

The Implementation of HomeItemBasic::FormattedDisplay

Let's start examining how we actually edit a HomeItemBasic by looking at Figure 11.40, which shows the new version of HomeItemBasic::FormattedDisplay.

Figure 11.40. HomeItemBasic::FormattedDisplay (from codehmit5.cpp)
short HomeItemBasic::FormattedDisplay(ostream &os)
{
 short FieldNumber = e_Name;

 os << "Type: " << GetType() << endl;

 os << FieldNumber << ". ";
 os << GetFieldName(FieldNumber) << ": ";
 FieldNumber ++;
 os << m_Name << endl;

 os << FieldNumber << ". ";
 os << GetFieldName(FieldNumber) << ": ";
 FieldNumber ++;
 os << m_PurchasePrice << endl;

 os << FieldNumber << ". ";
 os << GetFieldName(FieldNumber) << ": ";
 FieldNumber ++;
 os << m_PurchaseDate << endl;

 os << FieldNumber << ". ";
 os << GetFieldName(FieldNumber) << ": ";
 FieldNumber ++;
 os << m_Description << endl;

 os << FieldNumber << ". ";
 os << GetFieldName(FieldNumber) << ": ";
 FieldNumber ++;
 os << m_Category << endl;

 return FieldNumber;
}

This is almost identical to the HomeItemBasic version of ReadInteractive. The differences are these:

  1. It writes its output to the ostream specified by its argument, os, rather than to cout, as HomeItemBasic::ReadInteractive does.

  2. It doesn't prompt the user for input.

The Implementation of HomeItemBasic::EditField

Now let's look at the code for the HomeItemBasic version of the EditField function (Figure 11.41).

Figure 11.41. HomeItemBasic::EditField (from codehmit5.cpp)
bool HomeItemBasic::EditField(short FieldNumber)
{
 bool result = true;

 switch (FieldNumber)
  {
  case e_Name:
  cout << FieldNumber << ". ";
  cout << GetFieldName(FieldNumber) << ": ";
  getline(cin,m_Name);
  break;

  case e_PurchasePrice:
  cout << FieldNumber << ". ";
  cout << GetFieldName(FieldNumber) << ": ";
  cin >> m_PurchasePrice;
  break;

  case e_PurchaseDate:
  cout << FieldNumber << ". ";
  cout << GetFieldName(FieldNumber) << ": ";
  cin >> m_PurchaseDate;
  break;

  case e_Description:
  cout << FieldNumber << ". ";
  cout << GetFieldName(FieldNumber) << ": ";
  getline(cin,m_Description);
  break;

  case e_Category:
  cout << FieldNumber << ". ";
  cout << GetFieldName(FieldNumber) << ": ";
  getline(cin,m_Category);
  break;

  default:
  cout << "Sorry, that is not a valid field number." << endl;
  result = false;
  break;
  }

 return result;
}

This code probably looks a little odd. Where are all the if statements needed to select the field to be modified based on its field number? That would be one way to code this function, but I've chosen to use a different construct designed specifically to select one of a number of alternatives: the switch statement. This statement is essentially equivalent to a number of if/else statements in a row, but is easier to read and modify. Its syntax consists of the keyword switch followed by a selection expression in parentheses that specifies the value that will determine the alternative to be selected. The set of alternatives to be considered is enclosed in a set of curly braces; individual alternatives are marked off by the keyword case followed by the (constant) value to be matched and a colon. In the current situation, the selection expression is FieldNumber, whose value will be compared with the various case labels inside the curly brackets of the switch statement. For example, if the value of FieldNumber is equal to e_Name, the section of code following case e_Name: will be executed.

We use the break statement to indicate the end of the section of code to be executed for each case. We've already seen the break statement used to terminate a for loop, and it works in much the same way here: it breaks out of the curly braces containing the alternatives for the switch statement. It's also possible to terminate the code to be executed for a given case by executing a return statement to exit from the function. If we have already accomplished the purpose of the function, this is often a convenient alternative.

There's one more item I should mention: the default keyword, which begins a section of code that should be executed in the event that the value of the selection expression doesn't match any of the individual cases. This is very handy to catch programming errors that result in an invalid value for the selection expression. If we don't use default, the switch statement will essentially be skipped if there is no matching case, and that probably isn't the right thing to do. Therefore, it's a good idea to use default to catch such an error whenever possible in a switch statement.

In the current situation, we're using the default case to display an error message and return the value false to the calling function so that it will know that its attempt to edit the object didn't work.

However, if you have been following along very carefully, you'll notice that the function that calls this one, Edit, doesn't bother to check the return value from EditField, so it wouldn't notice if this error ever occurred. Such an omission doesn't cause any trouble here because the user has already been notified that his edit didn't work. Unfortunately, however, the very common problem of forgetting to check return values isn't always so benign. In fact, it's one of the main reasons for the introduction of exception handling to C++. However, we won't get a chance to discuss this important topic in this book, other than our brief discussion of what happens when operator new doesn't have any memory to give us.[14]

[14] That discussion starts on page 451.

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

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