CHAPTER 11 The Home Inventory Project
Now that we have enough tools to start working on a somewhat more realistic project, what sort of project should that be? I've spent some time thinking that question over and have come up with what I think is a good way to hone our skills as well as advance them: a "home inventory program" to keep track of the myriad objects, technically known as "stuff", that we have lying around in our houses or apartments. When you consider that insurance companies are a lot happier and quicker to pay after a loss if you have an up-to-date list of your possessions, with any luck this program may actually be useful as well as instructive! Of course before we start, we need to define some terms and establish some objectives for this chapter.
11.1. Definitions
Aliasing is the practice of referring to one object by more than one "name"; in C++ these names are actually pointers or references.
The aliasing problem is a name for the difficulties caused by altering a shared object.
An enum is a way to define a number of unchangeable values, which are quite similar to consts. 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). The term enum is short for "enumeration", which is a list of items.
The switch statement is functionally equivalent to a number of if/else statements in a row, but is easier to read and modify. The keyword switch is followed by a selection expression (in parentheses), which specifies an expression used to select an alternative section of code. The various alternatives to be considered are enclosed in a set of curly braces following the selection expression and are marked off by the keyword case followed by the (constant) value to be matched and a colon.
An array initialization list is a list of values that are used to initialize the elements of an array. 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.
11.2. Objectives of This Chapter
By the end of this chapter, you should
1. Understand how to create a more realistic application using the tools we already have,
2. Know how to use the switch statement to select among a number of alternatives,
3. Know how to use the enum data type to give names to constant values that will be used to refer to items in a list,
4. Understand the advantages of creating variables at the point of use rather than at the beginning of a function,
5. Know how to find or change the size of a Vec dynamically when necessary, and
6. Understand how to use virtual functions for implementation purposes within a polymorphic class hierarchy.
11.3. Data Items for the Home Inventory Project
What information will we need to store for the various types of objects in our home inventory? No matter what type of object we are recording, we will want to store the following data:
1. type of object,
2. date of acquisition,
3. price,
4. description,
5. category (e.g., office furniture, kitchen appliance).
While we'll have to maintain the above data for every object, we'll also need to keep track of other information on some types of objects. Of course, the exact form of that extra information will depend on each object's type. After some thought, I have come up with the following types of objects that we might want to keep track of (in no particular order):
1. "Basic" objects, for which the above data are sufficient;
2. "Music" objects (e.g., CDs, LPs, cassettes);
3. computer hardware;
4. computer software;
5. other electric and electronic appliances;
6. books;
7. kitchen items such as plates and flatware;
8. clothes and shoes.
Of course, each of these types has different additional type-specific information, which is why we need the individual types in the first place.
Undoubtedly you can think of a number of other kinds of objects that don't fit exactly into one of the listed types. However, I think most of these other kinds are close enough to one of the above types that we can use one of those listed without too much strain. For example, what about jewelry and art objects? It seems to me that the "Basic" type is fine for both of these, as we have to keep track of the basic information needed for all objects (value, date of acquisition, description, and category) and not much else.
Choosing Categories for Items
This brings us to a very important point in the design of any program, especially an object-oriented one: deciding how to fit the nearly infinite possibilities of real-world entities into a necessarily limited number of categories in the program. I wish there were a hard-and-fast rule I could give you so you wouldn't have to make this decision for every program you write; unfortunately, however, this isn't possible, precisely because there are so many possibilities in the real world. The best I can do is give guidelines and examples.
Consider the example of LP records or cassette tapes. In this case, I think it's pretty obvious that an LP or a cassette tape is similar enough to a CD that using the "Music" type for either of these other types of sound recording is appropriate.1 How did I make this determination?
1. The purpose of a CD is to contain music or speech. This is also the purpose of an LP or cassette.
2. The information that we might want to store about a CD includes artist, title, and track names. These are also appropriate for an LP or cassette.
Note that the storage medium and other surface similarities among objects aren't significant in this analysis. In fact, a CD-ROM, which uses exactly the same storage medium as a music CD does, is a completely different type of object from a music CD and needs to be categorized under "computer software". That's because the purpose of the objects and the information we need to store about these two different kinds of CDs are completely different.2
What about rare coins or stamps? If you had only a couple of either of these objects, you might very well use the "Basic" type to keep track of them. However, if you had an extensive collection, you probably would want to keep track of their condition, year of minting or printing, denomination and other data of interest to collectors. To handle this extra information, you would add a "Coin/Stamp" type or even two separate types, if you happened to collect both. This merely illustrates the rule that the handling of data has to be based on the use to which it will be put, not on its intrinsic characteristics alone.
Now that we've developed a general outline of these classes and the data that they need to keep track of, let's start designing the interface they will present to the application program that uses them.
11.4. The Manager/Worker Idiom Again
Why do I say "the interface" rather than "the interfaces"? Because we're going to implement these classes to give the appearance of a single type of object that can change its behavior; in other words, we're going to use the manager/worker idiom again to implement another type of polymorphic object. This will enable us to write a main program that allows access to an object of any of these types in a uniform manner. Of course, the implementation of the member functions will be somewhat different in each class, to take account of the peculiarities of that class, but the structure of the program will be essentially the same for any number of types once we've made provisions to handle more than one. For this reason, we'll use just the "Basic" and "Music" types in the text; handling other types will be left as an exercise. Figure 11.1 shows the initial HomeItem interface.
FIGURE 11.1. The initial interface for the HomeItem manager class (code/hmit1.h)
// hmit1.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: Includes 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);
protected:
HomeItem(int);
protected:
HomeItem* m_Worker;
short m_Count;
};
If you think this looks familiar, you're right. It's almost exactly the same as the polymorphic object version of the StockItem interface we saw in Chapter 10. This is not a coincidence; every polymorphic object interface is going to look very similar to every other one. Why is this?
Similarities and Differences between Polymorphic Objects
They all look alike because the objects of every polymorphic object manager class do very similar things: managing the "real" objects of classes derived from the manager class. The only differences between the interfaces of two polymorphic object types are in the member functions that the user of the polymorphic objects sees. In this case, we don't have a Reorder function as we did in the StockItem class, for the very simple reason that we don't have to figure out how many HomeItem objects to reorder from our distributors.
Before we get into the worker classes for the HomeItem polymorphic object, let's go over the similarities and differences between the StockItem and HomeItem interfaces.
1. The operators << and >>, as well as the default constructors, copy constructors, assignment operators, and destructors, have the same interfaces in the StockItem class and the HomeItem class except, of course, for their names and the types of their arguments (if applicable). This also applies to the "special" constructor used to prevent an infinite loop during construction of a worker object and to the Write function used to create a displayable and storable version of the data for an object.
2. The "normal" constructors that create objects for which the initial state is known are the same in these two classes except, of course, for the exact arguments, which depend on the data needed by each object. One point we'll cover later is the use of a Vec as an argument to the second "normal" constructor.
3. The GetName, GetPrice, and other class-specific member functions of StockItem don't exist in HomeItem because it is a different class with different requirements from those of StockItem.3
4. The member data items for the two classes are the same except, again, for the type of m_Worker, which is a pointer to a HomeItem rather than to a StockItem.
Of course, this class doesn't really do anything by itself; as with all polymorphic objects, we need the worker classes to get the job done. Figure 11.2 shows the interfaces for HomeItemBasic and HomeItemMusic.
FIGURE 11.2. The initial interface for the HomeItemBasic and HomeItemMusic worker classes (code\hmiti1.h)
// hmiti1.h
class HomeItemBasic : public HomeItem
{
public:
HomeItemBasic();
HomeItemBasic(std::string Name, double PurchasePrice,
long PurchaseDate, std::string Description, std::string Category);
virtual void Write(std::ostream& os);
virtual std::string GetType();
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();
protected:
std::string m_Artist;
Vec<std::string> m_Track;
};
Susan had a question about having two interface files.
Susan: Why do we need two different interface files again?
Steve: The first one (hmit1.h) is for the user of these classes; the second one (hmiti1.h) is only for our use as class implementers. The user never sees this second interface file, which means that we can change it if we need to without forcing the user to recompile everything.
As we did with the manager classes, let's take a look at the similarities and differences between the HomeItem and StockItem worker classes.
1. The default constructors, copy constructors, assignment operators, and destructors have the same interfaces in the StockItem worker classes and the HomeItem worker classes except, of course, for their names and the types of their arguments (if applicable). This also applies to the Write function used to create a displayable and storable version of the data for an object. By the way, the destructors are guaranteed to be virtual because the base class destructor is virtual. I've added the virtual keyword to the declaration of the derived class destructors solely for clarity.
2. The "normal" constructors that create objects for which the initial state is known are the same in all of these classes except, of course, for the exact arguments, which depend on the data needed by each object. Again, we'll go into what it means to have a Vec argument when we cover the implementation of the "normal" constructor for the HomeItemMusic class.
3. The HomeItem worker classes have a GetType member function that the StockItem classes don't have. The purpose of this function is to allow the proper storage and display of objects of various types. In the StockItem class, we depended on the value of the expiration date ("0" or a real date) to give us this information.
4. The GetName, GetPrice, and other class-specific member functions of the StockItem worker classes don't exist in the HomeItem worker classes, as explained above.
5. The member data items for the HomeItem worker classes are as needed for these classes, as with the HomeItem worker classes.
6. We are using the long data type for the m_PurchaseDate member variable rather than the string data type that we used for a similar field in DatedStockItem.4
7. We are using the double data type for the m_PurchasePrice member variable rather than the short data type that we used for price information in the StockItem classes.
The first of the differences between the StockItem worker classes and the HomeItem worker classes that needs additional explanation is the GetType virtual function first declared in HomeItemBasic. Since I have claimed that all the classes that participate in a polymorphic object implementation must have the same interface (so the user can treat them all in the same way), why am I declaring a new function in one of the worker classes that wasn't present in the base class?
11.5. Hiding Unnecessary Information from the class User
That rule applies only to functions that are accessible to the user of the polymorphic object. The GetType function is intended for use only in the implementation of the polymorphic object, not by its users; therefore, it is not only possible but desirable to keep it "hidden" by declaring it inside one of the worker classes. Because the user never creates an object of any of the worker classes directly, declaring a function in one of those classes has much the same effect as making it a private member function. As we have already seen, hiding as many implementation details as possible helps to improve the robustness of our programs.
I should also mention the different data types for member variables in the HomeItem classes that have similar functions to those in the StockItem classes. In HomeItemBasic, we are using a long to hold a date, where we used a string in the DatedStockItem class. A sufficient reason for this change is that in the current class, we are getting the date from the user, so we don't have the problem of converting the system date to a storable value as we did with the implementation of the former class. As for the double we're using to store the price information, that's a more sensible data type than short for numbers that may have decimal parts. I avoided using it in the earlier example only to simplify the presentation, but at this point I don't think it should cause you any trouble.
Aside from these details, this polymorphic object's definition is very similar to the one for the StockItem polymorphic object. The similarity between the interfaces (and corresponding similarity of implementations) of polymorphic objects is good news because it makes generating a new polymorphic object interface and basic implementation quite easy. It took me only a couple of hours to write the initial version of the HomeItem classes using StockItem as a starting point. What is even more amazing is that the test program (Figure 11.3) worked correctly the very first time I ran it!5
The Initial HomeItem Test Program
FIGURE 11.3. The initial test program for the HomeItem classes (code\hmtst1.cpp)
// hmtst1.cc
#include <iostream>
#include <fstream>
#include <string>
#include "Vec.h"
#include "hmit1.h"
using namespace std;
int main()
{
HomeItem x;
HomeItem y;
ifstream HomeInfo("home1.in");
HomeInfo >> x;
HomeInfo >> y;
cout << "A basic HomeItem: " << endl;
cout << x;
cout << endl;
cout << "A music HomeItem: " << endl;
cout << y;
return 0;
}
I don't think that program needs much explanation. It is exactly the same as the corresponding StockItem test program in Figure 10.21 on page 696, with the obvious exception of the types of the objects and the name of the ifstream used to read the data. Figure 11.4 shows the result of running the above program.
FIGURE 11.4. Results of running the first HomeItem test program (code\hmit1.out)
A basic HomeItem:
Basic
Living room sofa
1600
19970105
Our living room sofa
Furniture
A music HomeItem:
Music
Relish
12.95
19950601
Our first album
CD
Joan Osborne
2
Right Hand Man
Ladder
Now that we've gone over the interfaces for the classes that cooperate to make a polymorphic HomeItem object, as well as the first test program and its output, we can see the initial implementation in Figure 11.5.
FIGURE 11.5. Initial implementation of HomeItem manager and worker classes (code\hmit1.cpp)
// hmit1.cpp
#include <iostream>
#include <iomanip>
#include <sstream>
#include <string>
#include "Vec.h"
#include "hmit1.h"
#include "hmiti1.h"
using namespace std;
//friend functions of HomeItem
ostream& operator << (ostream& os, const HomeItem& Item)
{
Item.m_Worker->Write(os);
return os;
}
istream& operator >> (istream& is, HomeItem& Item)
{
string Type;
string Name;
double PurchasePrice;
long PurchaseDate;
string Description;
string Category;
while (Type == "")
{
getline(is,Type);
if (is.fail() != 0)
{
Item = HomeItem();
return is;
}
}
getline(is,Name);
is >> PurchasePrice;
is >> PurchaseDate;
is.ignore();
getline(is,Description);
getline(is,Category);
if (Type == "Basic")
{
Item = HomeItem(Name, PurchasePrice, PurchaseDate,
Description, Category);
}
else if (Type == "Music")
{
string Artist;
short TrackCount;
getline(is,Artist);
is >> TrackCount;
is.ignore();
Vec<string> Track(TrackCount);
for (short i = 0; i < TrackCount; i ++)
{
getline(is,Track[i]);
}
Item = HomeItem(Name, PurchasePrice, PurchaseDate,
Description, Category, Artist, Track);
}
else
{
cout << "Can't create object of type " << Type << endl;
exit(1);
}
return is;
}
// HomeItem member functions
HomeItem::HomeItem()
: m_Count(0), m_Worker(new HomeItemBasic)
{
m_Worker->m_Count = 1;
}
HomeItem::HomeItem(const HomeItem& Item)
: m_Count(0), m_Worker(Item.m_Worker)
{
m_Worker->m_Count ++;
}
HomeItem& HomeItem::operator = (const HomeItem& Item)
{
HomeItem* temp = m_Worker;
m_Worker = Item.m_Worker;
m_Worker->m_Count ++;
temp->m_Count --;
if (temp->m_Count <= 0)
delete temp;
return *this;
}
HomeItem::~HomeItem()
{
if (m_Worker == 0)
return;
m_Worker->m_Count --;
if (m_Worker->m_Count <= 0)
delete m_Worker;
}
HomeItem::HomeItem(string Name, double PurchasePrice,
long PurchaseDate, string Description,
string Category)
: m_Count(0),
m_Worker(new HomeItemBasic(Name, PurchasePrice,
PurchaseDate, Description, Category))
{
m_Worker->m_Count = 1;
}
HomeItem::HomeItem(int)
: m_Worker(0)
{
}
HomeItem::HomeItem(string Name, double PurchasePrice,
long PurchaseDate, string Description,
string Category, string Artist,
Vec<string> Track)
: m_Count(0),
m_Worker(new HomeItemMusic(Name, PurchasePrice,
PurchaseDate, Description, Category, Artist, Track))
{
m_Worker->m_Count = 1;
}
void HomeItem::Write(ostream& )
{
exit(1); // error
}
// HomeItemBasic member functions
HomeItemBasic::HomeItemBasic()
: HomeItem(1),
m_Name(),
m_PurchasePrice(0),
m_PurchaseDate(0),
m_Description(),
m_Category()
{
}
HomeItemBasic::HomeItemBasic(string Name,
double PurchasePrice, long PurchaseDate,
string Description, string Category)
: HomeItem(1),
m_Name(Name),
m_PurchasePrice(PurchasePrice),
m_PurchaseDate(PurchaseDate),
m_Description(Description),
m_Category(Category)
{
}
void HomeItemBasic::Write(ostream& os)
{
os << GetType() << endl;
os << m_Name << endl;
os << m_PurchasePrice << endl;
os << m_PurchaseDate << endl;
os << m_Description << endl;
os << m_Category << endl;
}
string HomeItemBasic::GetType()
{
return "Basic";
}
// HomeItemMusic member functions
HomeItemMusic::HomeItemMusic(string Name,
double PurchasePrice, long PurchaseDate,
string Description, string Category,
string Artist, Vec<string> Track)
: HomeItemBasic(Name,PurchasePrice,PurchaseDate,
Description, Category),
m_Artist(Artist),
m_Track(Track)
{
}
void HomeItemMusic::Write(ostream& os)
{
HomeItemBasic::Write(os);
os << m_Artist << endl;
int TrackCount = m_Track.size();
os << TrackCount << endl;
for (short i = 0; i < TrackCount; i ++)
os << m_Track[i] << endl;
}
string HomeItemMusic::GetType()
{
return "Music";
}
What does this first version of HomeItem do for us? Not too much; it merely allows us to read HomeItem objects from a file, display them, and write them out to a file. Although we've seen the implementation of similar functions in the StockItem class, it should still be worthwhile to discuss how these are similar to and different from the corresponding functions in the HomeItem classes. However, to avoid too much repetition we'll skip the functions that are essentially identical in these two cases, including the following functions for the base class, HomeItem:
1. operator <<;
2. the copy constructor;
3. the default constructor;
4. operator =;
5. the destructor;
6. the normal constructors that create worker objects with known initial data;
7. the "special" constructor that prevents an infinite regress when creating a worker object.
Here's a list of the functions we'll skip for the HomeItemBasic and HomeItemMusic classes:
1. the default constructor;
2. the copy constructor;
3. operator =;
4. the normal constructor;
5. the destructor.
The first HomeItem function we'll discuss is Write, shown in Figure 11.6.
FIGURE 11.6. HomeItem::Write (from code\hmit1.cpp)
void HomeItem::Write(ostream& )
{
exit(1); // error
}
Calling this function is an error and will cause the program to exit. In case you're wondering why this is an error, you may be happy to know that Susan had the same question.
Susan: Why is calling HomeItem::Write an error?
Steve: Because this function exists solely for use by operator << in writing out the data for a derived class object. Therefore, if it is ever called for a HomeItem base class object, we know that someone has used the function incorrectly, and we leave the program before any more incorrect processing can occur.
The next function we need to look at is operator >>, shown in Figure 11.7.
FIGURE 11.7. The HomeItem implementation of operator >> (from code\hmit1.cpp)
istream& operator >> (istream& is, HomeItem& Item)
{
string Type;
string Name;
double PurchasePrice;
long PurchaseDate;
string Description;
string Category;
while (Type == "")
{
getline(is,Type);
if (is.fail() != 0)
{
Item = HomeItem();
return is;
}
}
getline(is,Name);
is >> PurchasePrice;
is >> PurchaseDate;
is.ignore();
getline(is,Description);
getline(is,Category);
if (Type == "Basic")
{
Item = HomeItem(Name, PurchasePrice, PurchaseDate,
Description, Category);
}
else if (Type == "Music")
{
string Artist;
short TrackCount;
getline(is,Artist);
is >> TrackCount;
is.ignore();
Vec<string> Track(TrackCount);
for (short i = 0; i < TrackCount; i ++)
{
getline(is,Track[i]);
}
Item = HomeItem(Name, PurchasePrice, PurchaseDate,
Description, Category, Artist, Track);
}
else
{
cout << "Can't create object of type " << Type << endl;
exit(1);
}
return is;
}
This is quite similar in outline to the corresponding function in StockItem in that it reads data from an input source and creates a worker object of the correct type based on the value of one of the fields. However, this function does have a few noticeable differences from the StockItem version:
1. This function skips any empty lines that may be in the input file before reading the type of the object.
2. The type is specified explicitly as an extra field ("Basic" or "Music" in the current cases), rather than by the use of a special "0" value for the expiration date field, as in the DatedStockItem input file.
3. This function calls istream::fail() to determine whether the program has attempted to read more information from the input file than it contains (or if any other error has occurred when trying to read from the input file). If such an error occurs, the operator >> function assigns a default-constructed HomeItem to the reference argument Item and returns to the calling function immediately.
4. The local variables Artist, TrackCount, and Track are created only when the function needs them (for a "Music" object) rather than at the beginning of the function as has been our practice until now.
5. In the case of a "Music" object, one of those local variables is a Vec of strings.
6. The index variable i is also created only when it is needed, at the beginning of the for loop.
Susan had a question about the first of these differences.
Susan: Why would you want to put blank lines in the input file?
Steve: To make it easier to read. Because our StockItem input file couldn't have any blank lines in it, the data for each item started right after the end of the data for the previous item, which makes it hard for a human being to tell what the entries in the input file mean. Of course the program doesn't care, but sometimes it's necessary for a person to look at the input file, especially when there's something wrong with it!
The reason for the second difference from the StockItem version of operator >> is fairly simple. All of the types of HomeItems share the basic data in the HomeItemBasic class, so we don't have any otherwise unused field that we can use to indicate which actual type the object belongs to, as we did with the date field in the StockItem case; thus, we have to add another field to explicitly specify that type.
However, the other differences are a bit more interesting. First, part of the reason that we have to check for the input stream terminating (or "failing") here, when we didn't have to do that in the StockItem case, is that we're trying to skip blank lines between the data for successive objects. This means that when we reach the end of the file, we might have blank lines that we might try to read while looking for the next set of data; if there isn't any more data, we might run off the end of the file, if we don't check for this condition. I added this "blank line skipping" feature of operator >> to make the input files easier to read and write for human beings, but the way I originally implemented it had an unexpected side effect; the program looped forever if I gave it a bad file name. In fact, it always did this at the end of the file! Figure 11.8 is my original implementation; see if you can tell what's wrong with it.
FIGURE 11.8. The (incorrect) while loop in the original implementation of operator >>
This won't work if the file tied to is doesn't exist or if we've already reached the end of that file, because the statement getline(is,Type); won't change the value of Type in either of those cases. Therefore, Type will retain its original value, which is "" (the empty string). Since the loop continues as long as Type is the empty string, it becomes an endless loop and the program will "hang" (run forever). The solution is simple enough: use fail to check if the stream is still working before trying to read something else from it.
Of course, if the stream has stopped working, we can't get any data to put into a HomeItem object; in that case, we set the reference argument Item to the value of a default-constructed HomeItem (so that the calling function can tell that it hasn't received a valid HomeItem) and return immediately.
Now that we've cleared that up, let's examine why it is more than just a convenience to be able to create new local variables at any point in a function.
Creating Local Variables When They Are Needed
There are a couple of reasons to create the local variable Track only after we detect that we're dealing with a "Music" object. First, it's relatively time-consuming to create a Vec, especially one of a non-native data type like string, because each string in the Vec has to be created and placed in the Vec before it can be used. But a more significant reason is that we don't know how large the Vec needs to be until we have read the "track count" from the file. As you'll see later, it's possible (and even sometimes necessary) to change the size of a Vec after it has been created, but that takes extra work that we should avoid if we can. Therefore, it is much more sensible to wait until we have read the count so that we can create the Vec with a size that is just right to hold all of the track names for the CD, LP, or cassette.
Susan had a question about reading the track names from the file.
Susan: I don't get how the code works to read in the track names from the file when there can be different numbers of tracks for each album.
Steve: That's what these lines are for:
Vec<string> Track(TrackCount);
for (short i = 0; i < TrackCount; i ++)
First, we read the number of tracks from the file into the variable TrackCount. Next, we create a Vec to hold that many strings (the track names). Finally, we use the loop to read all of the track names into the Vec.
Susan: Okay, I get it.
The final reason to wait as long as possible before creating the Vec of strings to hold the track names is that we don't need it at all if we're not creating a "Music" object, so any effort to create it in any other case would be a complete waste. Although we don't have to be fanatical about saving computer time, doing work that doesn't need to be done is just silly.
The Scope of an Index Variable
The creation of an index variable such as i during the initialization of a for loop is also a bit more significant than it looks, because the meaning of this operation changed recently. Earlier versions of C++ had an inadvertent "feature" that was fixed in the final standard: a variable created in the initialization section of a for loop used to exist from that point until the end of the block enclosing the for loop, not just during the for loop's execution. This old rule was replaced by the more logical rule that the scope of a variable created in the initialization section of a for loop consists of the for loop's header and the controlled block of the for loop. Thus, the program in Figure 11.9 was illegal under the old rule because you can't have two local variables named i in the same scope and, under the old rule, the scopes of the two i's were overlapping. However, it is perfectly legal under the new rule because the scopes of the two i's are separate.6
Susan had a question about creating a variable in the header of a for loop.
Susan: What does "for (short i=0" mean that is different from just "for (i=0"?
Steve: The former phrase means that we're creating a new variable called i that will exist only during the execution of the for loop; the latter one means that we're using a preexisting variable called i for our loop index.
Susan: Why would you want to use one of these phrases rather than the other?
Steve: You would generally use the first one because it's a good idea to limit the scope of a variable as much as possible. However, sometimes you need to know the last value that the loop index had after the loop terminated; in that case, you have to use the latter method so that the index variable is still valid after the end of the for loop.
FIGURE 11.9. A legal program (code\fortest.cpp)
for (short i = 0; i < 10; i ++)
for (short i = 0; i < 10; i ++)
One of the Oddities of the Value 0
Before we get to the derived class member functions of HomeItem that have significant changes from the StockItem versions, I want to point out one of the oddities of using the value 0 in C++. Figure 11.10 shows an incorrect version of the HomeItemBasic default constructor.
FIGURE 11.10. An incorrect default constructor for the HomeItemBasic class (strzero.err)
HomeItemBasic::HomeItemBasic()
What's wrong with this picture? The 0 values in the m_Description and m_Category member variable initializers. These are string variables, so how can they be set to the value 0?
The answer is that, as we've discussed briefly in Chapter 10, 0 is a "magic number" in C++. In this case, the problem is that 0 is a legal value for any type of pointer, including char*. Because the string class has a constructor that makes a string out of a char*, the compiler will accept a 0 as the value in a string variable initializer. Unfortunately, the results of specifying a 0 in such a case are undesirable; whatever data happens to be at address 0 will be taken as the initial data for the string, which will cause havoc whenever you try to use the value of that string. Therefore, we have to be very careful not to supply a 0 as the initial value for a string variable.
Susan had a question about this issue.
Susan: Why is this even legal?
Steve: Well, it isn't exactly legal (the standard says the results are "undefined"), but the compiler isn't required to prevent you from doing it even when it could (as in this case). I think that is a defect in the language.
Susan: Then they should fix it.
Steve: I agree with you. Maybe I'll have to see about joining the standards committee to lobby for this change. In fact, I'd like to get rid of most of the "magical" properties of 0. They are a rich source of error and confusion and have vastly outlived their usefulness.
Now that I've warned you about that problem, let's take a look at the other functions pertaining to the HomeItem manager and worker classes that differ from those in the StockItem classes. The first two of these functions are HomeItemBasic::GetType (Figure 11.11) and HomeItemMusic::GetType (Figure 11.12). Each of these functions returns a string representing the type of the object to which it is applied, which in this case is either "Basic" or "Music".
FIGURE 11.11. HomeItemBasic::GetType (from code\hmit1.cpp)
string HomeItemBasic::GetType()
FIGURE 11.12. HomeItemMusic::GetType (from code\hmit1.cpp)
string HomeItemMusic::GetType()
{
return "Music";
}
In case it's not obvious why we need these functions, the explanation of the Write functions for the two worker classes should clear it up. Let's start with HomeItemBasic::Write (Figure 11.13).
FIGURE 11.13. HomeItemBasic::Write (from code\hmit1.cpp)
void HomeItemBasic::Write(ostream& os)
{
os << GetType() << endl;
os << m_Name << endl;
os << m_PurchasePrice << endl;
os << m_PurchaseDate << endl;
os << m_Description << endl;
os << m_Category << endl;
}
This function writes out all the data for the object it was called for, as you might expect. But what about that first output line, os << GetType() << endl;, which gets the type of the object to be written via the GetType function? Isn't the object in question obviously a HomeItemBasic?
Reusing Code Via a virtual Function
In fact, it may be an object of any of the HomeItem classes - say, a HomeItemMusic object - because the HomeItemBasic::Write function is designed to be called from the Write functions of other HomeItem worker classes. For example, it is called from HomeItemMusic::Write, as you can see in Figure 11.14.
FIGURE 11.14. HomeItemMusic::Write (from code\hmit1.cpp)
void HomeItemMusic::Write(ostream& os)
{
HomeItemBasic::Write(os);
os << m_Artist << endl;
int TrackCount = m_Track.size();
os << TrackCount << endl;
for (short i = 0; i < TrackCount; i ++)
os << m_Track[i] << endl;
}
It would be redundant to make the HomeItemMusic::Write function display all the data in a HomeItemBasic object. After all, the HomeItemBasic version of Write already does that.
Susan wasn't convinced that reusing HomeItemBasic::Write was that important.
Susan: So who cares if we have to duplicate the code in HomeItemBasic::Write? It's only a few lines of code anyway.
Steve: Yes, but duplicated code is a prescription for maintenance problems later. What if we add another five or six data types derived from HomeItemBasic? Should we duplicate those few lines in every one of their Write functions? If so, we'll have a wonderful time tracking down all of those sets of duplicated code when we have to make a change to the data in the base class part!
Assuming this has convinced you of the benefit of code reuse in this particular case, we still have to make sure of one detail before we can reuse the code from HomeItemBasic::Write: the correct type of the object has to be written out to the file.
Remember, to read a HomeItem object from a file, we have to know the appropriate type of HomeItem worker object to create, which is determined by the type indicator (currently "Basic" or "Music") in the first line of the data for each object in the input file. Therefore, when we write the data for a HomeItem object out to the file, we have to specify the correct type so that we can reconstruct the object properly when we read it back in later. That's why we use the GetType function to get the type from the object in the HomeItemBasic::Write function; if the HomeItemBasic::Write function always wrote "Basic" as the type, the data written to the file wouldn't be correct when we called HomeItemBasic::Write to write out the common parts of any HomeItem worker object. As it is, however, when HomeItemBasic::Write is called from HomeItemMusic::Write, the type is correctly written out as "Music" rather than as "Basic", because the GetType function will return "Music" in that case.
The key to the successful operation of this mechanism, of course, is that GetType is a virtual function. Therefore, when we call GetType from HomeItemBasic::Write, we are actually calling the appropriate GetType function for the HomeItem worker object for which HomeItemBasic::Write was called (i.e., the object pointed to by this). Because each of the HomeItemBasic worker object types has its own version of GetType, the call to GetType will retrieve the correct type indicator.
By this point, Susan was apparently convinced that using HomeItemBasic::Write to handle the common parts of any HomeItem object was a good idea, but that led to the following exchange.
Susan: Why didn't we do this with the StockItem classes?
Steve: Because I wrote separate Write functions for the different StockItem classes.
Susan: You should have done it this way.
Steve: Yes, you're right, but I didn't think of it then. I guess that proves that you can always improve your designs!
After we account for this important characteristic of the Write functions, the rest of HomeItemMusic::Write is pretty simple, except for one function that we haven't seen before: size, which is a member function of Vec. This function returns the number of elements of the Vec, which we can then write to the output file so that when we read the data back in for this object, we'll know how big to make the Vec of track information.7
We have covered the member functions of the first version of our HomeItem polymorphic objects, so now let's add a few more features. Obviously, it would be useful to be able to search through all of the items of a given type to find one that matches a particular description. For example, we might want to find a HomeItemMusic object (such as a CD) that has a particular track on it.
Susan had a question about our handling of different types of objects.
Susan: I thought we weren't supposed to have to know whether we were dealing with a HomeItemMusic or a HomeItemBasic object.
Steve: Well, that depends on the context. The application program shouldn't have to treat these two types differently when the difference can be handled automatically by the code for the different worker types (e.g., when asking for them to be displayed on the screen), but the user definitely will need to be able to distinguish them sometimes (e.g., when looking for an album that has a particular track on it). The idea is to confine the knowledge of these differences to situations where they matter rather than having to worry about them throughout the program.
As we saw in the StockItem situation, it's not feasible to have a member function of HomeItem that looks for a particular HomeItem, because a member function needs an object of its class to work on and we don't know which object that is when we're doing the search; if we did, we wouldn't be searching!
For that reason, we have to create another class we'll call HomeInventory. This class contains a Vec of HomeItems, which the search functions examine whenever we look for a particular HomeItem.8
Why do I say "search functions" rather than "search function"? Because there are several ways that we might want to specify the HomeItem we're looking for. One way, of course, would be by its name, which presumably would be distinct for each HomeItem in our list.9 However, we might also want to find all the HomeItems in the Furniture category, or even all the HomeItems in the Furniture category that have the color "red" in their description.
To implement these various searches, we will need several search functions. A good place to start is with the simplest one, which searches for a HomeItem with a given name. We'll call this function FindItemByName. Let's take a look at the first version of the interface of the HomeInventory class, which includes this member function (Figure 11.15).
FIGURE 11.15. The initial HomeInventory class interface (code\hmin2.h)
class HomeInventory
{
public:
HomeInventory();
short LoadInventory(std::ifstream& is);
HomeItem FindItemByName(std::string Name);
private:
Vec<HomeItem> m_Home;
};
This is a pretty simple interface because it doesn't allow us to do anything other than load the inventory from a disk file into the Vec called m_Home (LoadInventory) and find an item in that Vec given the name of the item (FindItemByName). However, the implementation is a little less obvious, as suggested by the fact that there is no member data item to keep track of the number of elements in the m_Home Vec. To see how this works, let's take a look at the implementation of the HomeInventory class (Figure 11.16).
FIGURE 11.16. The initial implementation of HomeInventory (code\hmin2.cpp)
#include <iostream>
#include <fstream>
#include <string>
#include "Vec.h"
#include "hmit2.h"
#include "hmin2.h"
using namespace std;
HomeInventory::HomeInventory()
: m_Home (Vec<HomeItem>(0))
{
}
short HomeInventory::LoadInventory(ifstream& is)
{
short i;
for (i = 0; ; i ++)
{
m_Home.resize(i+1);
is >> m_Home[i];
if (is.fail() != 0)
break;
}
m_Home.resize(i);
return i;
}
HomeItem HomeInventory::FindItemByName(string Name)
{
short i;
bool Found = false;
short ItemCount = m_Home.size();
for (i = 0; i < ItemCount; i ++)
{
if (m_Home[i].GetName() == Name)
{
Found = true;
break;
}
}
if (Found)
return m_Home[i];
return HomeItem();
}
Note that we are creating the m_Home Vec in the HomeInventory constructor with an initial size of 0. Clearly, this can't be the final size because we almost certainly want to keep track of more than zero items!
The question, of course, is how many items we are going to have. The way the input file is currently laid out, there isn't any way to know how many items we will have initially until we've read them all from the input file. For that matter, even after we have read them all, we may still want to add items at some other point in the program. Therefore, we have two choices when designing a class like this:
1. Establish a Vec containing a fixed number of elements and keep track of how many of them are in use.
2. Resize the Vec as needed to hold as many elements as we need, using the size member function to keep track of how large it is.
Until this point, we've taken option 1, mainly because it's easier to explain. However, I think it's time to learn how we can take advantage of the more flexible second option, including some of the considerations that make it a bit complicated to use properly.
We will go over the LoadInventory function (shown in Figure 11.16) in some detail to see how this dynamic sizing works (and how it can lead to inefficiencies) as soon as we have dealt with another question Susan had about how we decide whether to declare loop index variables in the loop or before it starts.
Susan: Why are we saying short i; at the beginning of the function here instead of in the for loop?
Steve: Because we will need the value of i after the end of the loop to tell us how many items we've read from the file. If we declared i in the for loop header, we wouldn't be able to use it after the end of the loop.
With that cleared up, let's start with the first statement in the loop, m_Home.resize(i+1);. This sets the size of the Vec m_Home to one more than the current value of the loop index i. Because i starts at 0, on the first time through the loop the size of m_Home is set to 1. Then the statement is >> m_Home[i]; reads a HomeItem from the input file into element i of the m_Home Vec; the first time through the loop, that element is m_Home[0].
Actually, I oversimplified a little bit when I said that the line we just discussed "reads a HomeItem from the input file". To be more precise, it attempts to read a HomeItem from the input file. As we saw in our analysis of the operator >> function that we wrote to read HomeItems from a file, that operator can fail to return anything; in fact, failure is guaranteed when we try to read another HomeItem from the file when there aren't any left. Therefore, the next two lines
check for this possibility. When we do run out of data in the file, which will happen eventually, the break statement terminates the loop. Finally, the two lines
reset the number of elements in the Vec to the exact number that we've read successfully and return the result to the calling program in case it wants to know how many items we have read.
Susan had some questions about this process.
Susan: So, what we're doing here is setting aside memory for the HomeItem objects?
Steve: Yes, and we're also loading them from the file at the same time. These two things are connected because we don't know how much memory to allocate for the items before we've read all of them from the file.
The Problem with Calling resize Repeatedly
This is definitely a legal way to fill up a Vec with a number of data elements when we don't know in advance how many we'll have, but it isn't very efficient. The problem is in the way we are using the innocent-looking resize function: to resize the Vec every time we want to add another element. Every time we resize a Vec, it has to call new to allocate enough memory to hold the number of elements of its new size; it also has to call delete to release the memory it was using before. Thus, if we resize a Vec 100 times (for example) to store 100 elements, we are doing 100 news and 100 deletes. This is a very slow operation compared to other common programming tasks such as arithmetic, looping, and comparison, so it is best to avoid unnecessary memory reallocations.
Susan had some questions about reallocating memory.
Susan: I don't understand this idea of reallocating memory.
Steve: When we create a Vec, we have to say how many elements it can hold so that the code that implements the Vec type knows how much room to allocate for the information it keeps about each of those elements. When we increase the number of elements in the Vec, the resize member function has to increase the size of the area it uses to store the information about the elements. The resize member function handles this by allocating another piece of memory big enough to hold the information for all of the elements that can be stored in the new size, copying all the information it previously held into that new space, and then freeing the original piece of memory. Therefore, every time we change the size of a Vec, the resize function has to do an allocation, a copy, and a deallocation. This adds up to a lot of extra work that is best avoided if we don't have to do it all the time.10
Susan: Okay. Does this reallocation occur every time the user tries to look something up in the inventory?
Steve: No, just when we're adding an item or reading items from the file.
Luckily, there is a way to prevent this potential source of inefficiency, which we've already employed in a slightly different part of this program. If you'd like to try to figure it out yourself, stop here and think about it.
11.6. Saving the Number of the Elements in the File
Give up? Okay, here it is: when we create the file that contains the data for the HomeItem objects, we can start by writing the number of HomeItem objects as the first line of the file. This is the solution we used to preallocate the m_Track Vec that holds the track names for a "Music" HomeItem. The disadvantage of this solution is that it is harder to apply when the input file is generated directly by a human being, who is likely to make a mistake in counting the elements. However, this is not much of a drawback when we consider that the most common way to generate such a file in the real world is to create, edit, and delete items via a program. This program will read any pre-existing data file, allow modifications to the items from the file, and write out the updated data to the file so that it will be there the next time we start the program. Of course, such a program provides other facilities such as producing reports and searching for individual items, but as long as we're maintaining the whole database in memory, those functions don't have to worry about the structure of the file.
Susan had some questions about the inventory file.
Susan: What file are you talking about?
Steve: The file that holds the information about all of the HomeItem objects in the inventory.
Susan: How was that file created?
Steve: Either by writing it with a text editor or by adding objects using the AddItem function and then telling the program to write it out.
Susan: How does the program know where the data for each item starts?
Steve: Our implementation of operator >> knows how many fields there are for each object; when the data for one object is finished, the data for the next object must be coming up next in the file.
Figure 11.17 is the version of the LoadInventory function that uses a file whose first line is the count of items.
FIGURE 11.17. Yet another implementation of LoadInventory (from code\hmin3.cpp)
short HomeInventory::LoadInventory(ifstream& is)
{
short i;
short ElementCount;
is >> ElementCount;
is.ignore();
m_Home.resize(ElementCount+1);
for (i = 0; ; i ++)
{
is >> m_Home[i];
if (is.fail() != 0)
break;
}
if (i < ElementCount)
{
cerr << "Not enough items in input file" << endl;
exit(1);
}
m_Home.resize(ElementCount);
return i;
}
The first part of this should be fairly obvious; we are reading the number of elements from the file into a variable called ElementCount, and then ignoring the end of line character, as we must always do after reading a numerical value. However, the next statement might not be so obvious; it sets the size of the Vec to one more than the number of items that we expect to read. Why do we need an extra element in the Vec?
Handling the End-of-File Condition Properly
If we were to allocate exactly enough elements to store the data that we read from the file, we wouldn't be able to try to read one more element so that we could tell that we had reached the end of the file. The problem is that an attempt to use a Vec element that doesn't exist produces an "invalid element number" error from the Vec code, so we would never reach the statement that calls fail to find out that we are at the end of the file. For this reason, I've added one element to the number of items that we actually expect so that we can tell if we have reached the end of file on schedule.
Of course, after we have finished reading all the data and have reached the end of the file, we must make sure that we have read the number of items we expected. If we're short one or more items, we display an error and exit from the program; on the other hand, if the number of items is correct, we reset the size of the m_Home Vec to that number and return to the calling function.
Susan had a question about the way the error message was displayed.
Susan: What's cerr?
Steve: That's another automatically created ostream object, like cout. The difference is that you can make the output from cout go to a different file in a number of ways, both in the program and outside it. However, doing that doesn't affect where cerr sends its data. In other words, even if you change where the "normal" output goes, cerr will still send its data to the screen where the user can see the messages.
So that explains how the error message for a short file is displayed. However, we still need to consider the other possibility: having more items in the file than were supposed to be there. Why is this important? Because if there were actually more items in the file and we continued processing the data without telling the user about this problem, the information for those remaining items would be lost when we rewrote the file at the end of the program; obviously, that would be a serious mistake. It's almost always better to program "defensively" when possible rather than to assume that everything is as it is supposed to be and that no one has made any errors in the data.
So what will happen if there are more items in the file than there were supposed to be? We get an error from the Vec code, because we try to read into an element of the Vec that doesn't exist. Therefore, that possibility is covered.
Ignoring the possibility of errors in the data is just one way to produce a system that is overly susceptible to errors originating outside the code. Such errors can also result from the program being used in unexpected ways or even from the seemingly positive situation of a program with an unexpectedly long service life, as has occurred in some cases when the century part of the date changed from "19" to "20" (i.e., the "Year 2000 problem").
11.7. Creating a Data File Programmatically
After that discussion of "errors, their cause and cure", let's get back to the program. The next thing we're going to add is a way for the user to enter, modify, and delete information for home inventory items without having to manually create or edit a data file.
Susan thought I had something against data files. I cleared up her confusion with the following discussion.
Susan: What's wrong with data files?
Steve: Nothing's wrong with them. What's wrong is making the user type everything in using a text editor; instead, we're going to give the user the ability to create the data file with a data entry function designed for that purpose.
Susan: Oh, so we're creating a database?
Steve: You could say that. Its current implementation is pretty primitive, but could be upgraded to handle virtually any number of items if that turned out to be necessary.
Let's start with the ability to enter data for a new object, as that is probably the first operation a new user will want to perform.
Figure 11.18 shows the new header file for the HomeInventory class, which includes the new AddItem member function.
FIGURE 11.18. The next interface for the HomeInventory class (code\hmin4.h)
class HomeInventory
{
public:
HomeInventory();
short LoadInventory(std::ifstream& is);
HomeItem FindItemByName(std::string Name);
HomeItem AddItem();
private:
Vec<HomeItem> m_Home;
};
How did I decide on the signature of the AddItem member function? Well, it seems to me that the result of this function should be the HomeItem that it creates. As for the arguments (or lack thereof), the data for the new item is going to come from the user of the program via the keyboard (i.e., cin), so it doesn't seem necessary to provide any other data to the function when it is called in the program.
Susan wanted to make sure she knew what "user" meant in this context.
Susan: Is that the end user or the programmer who is using the HomeItem class?
Steve: Good question. In this case, it's the end user of the program.
The implementation of this new function is shown in Figure 11.19.
FIGURE 11.19. The AddItem member function of HomeInventory (from code\hmin4.cpp)
HomeItem HomeInventory::AddItem()
{
HomeItem TempItem = HomeItem::NewItem();
short OldCount = m_Home.size();
m_Home.resize(OldCount + 1);
m_Home[OldCount] = TempItem;
return TempItem;
}
The first statement of this function, HomeItem TempItem = HomeItem::NewItem();, creates a new HomeItem object called TempItem.
Susan had some questions about that statement.
Susan: Why is TempItem a HomeItem?
Steve: Because that's the type of object we use to keep track of the items in our home inventory.
Susan: I don't get how HomeItem is a type. It should be an object.
Steve: A class defines a new type of object. A type like HomeItem could be compared to a common noun like "cat", whereas the objects of that type resemble proper nouns like "Bonsai". You wouldn't say that you have "cat", but you might say that you have "a cat named Bonsai". Similarly, you wouldn't say that your program has HomeItem, but that it has a HomeItem called (in this case) TempItem.11
The initial value for TempItem is the return value of the call to HomeItem::NewItem();. The reason we have to specify the class of this function (HomeItem) is that it is a member function of the HomeItem class, not of the HomeInventory class. But what kind of function call is HomeItem::NewItem()? It obviously isn't a normal member function call because there's no object in front of the function name NewItem.
This is a static member function call. You may recall from Chapter 9 that a static member function is one for which we don't need an object. Our previous use of this type of function was in the Today function, which returns today's date; clearly, today's date doesn't depend on which object we are referring to. However, this type of member function is also convenient in cases such as the present one, where we are creating an object from keyboard input and therefore don't have the object available for use yet.
Before we get started on the implementation of the static member function called NewItem, let's take a look at the new interface for the HomeItem class, which is shown in Figure 11.20.
FIGURE 11.20. The new interface for HomeItem (code\hmit4.h)
// hmit4.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 void FormattedDisplay(std::ostream& os);
virtual std::string GetName();
static HomeItem NewItem();
protected:
HomeItem(int);
protected:
HomeItem* m_Worker;
short m_Count;
};
We'll get to some of the changes between the previous interface and this one as soon as we get through with the changes in the implementation needed to allow data input from the keyboard. The first part of this implementation is the code for HomeItem::NewItem(), which is shown in Figure 11.21.
FIGURE 11.21. The implementation of HomeItem::NewItem() (from code\hmit4.cpp)
HomeItem HomeItem::NewItem()
{
HomeItem TempItem;
cin >> TempItem;
return TempItem;
}
As you can see, this is a very simple function, as it calls operator >> to do all the real work; however, I had to modify operator >> to make this possible. Susan wanted to know what was wrong with the old version of operator >>.
Susan: Why do we need another new version of operator >>? What was wrong with the old one?
Steve: The previous version of that operator wasn't very friendly to the user who was supposed to be typing data at the keyboard. The main problem is that it didn't tell the user what to enter or when to enter it; it merely waited for the user to type in the correct data.
I fixed this problem by changing the implementation of operator >> to the one shown in Figure 11.22.
FIGURE 11.22. The new version of operator >> (from code\hmit4.cpp)
istream& operator >> (istream& is, HomeItem& Item)
{
string Type;
string Name;
double PurchasePrice;
long PurchaseDate;
string Description;
string Category;
bool Interactive = (&is == &cin);
while (Type == "")
{
if (Interactive)
cout << "Type (Basic, Music) ";
getline(is,Type);
if (is.fail() != 0)
{
Item = HomeItem();
return is;
}
}
if (Interactive)
cout << "Name ";
getline(is,Name);
if (Interactive)
cout << "Purchase Price ";
is >> PurchasePrice;
if (Interactive)
cout << "Purchase Date ";
is >> PurchaseDate;
is.ignore();
if (Interactive)
cout << "Description ";
getline(is,Description);
if (Interactive)
cout << "Category ";
getline(is,Category);
if (Type == "Basic")
{
Item = HomeItem(Name, PurchasePrice, PurchaseDate,
Description, Category);
}
else if (Type == "Music")
{
string Artist;
short TrackCount;
if (Interactive)
cout << "Artist ";
getline(is,Artist);
if (Interactive)
cout << "TrackCount ";
is >> TrackCount;
is.ignore();
Vec<string> Track(TrackCount);
for (short i = 0; i < TrackCount; i ++)
{
if (Interactive)
cout << "Track # " << i + 1 << ": ";
getline(is,Track[i]);
}
Item = HomeItem(Name, PurchasePrice, PurchaseDate,
Description, Category, Artist, Track);
}
else
{
cout << "Can't create object of type " << Type << endl;
exit(1);
}
return is;
}
I think most of the changes to this function should be fairly obvious. Assuming that we are reading data from the keyboard (i.e., Interactive is true), we have to let the user know what item we want typed in by displaying a prompt message such as "Name: " or "Category: ".
Susan had an excellent question about the implementation of this function.
Susan: How does it know whether input is from a file or the keyboard?
Steve: By testing whether the istream that we're reading from is the same as cin.
This test is performed by the statement
bool Interactive = (&is == &cin);
which defines a variable of type bool called Interactive that is initialized to the value of the expression (&is == &cin). What is the value of that expression going to be?
Comparing Two streams
The value is the result of applying the comparison operator, ==, to the arguments &is and &cin. As always with comparison operators, the type of this result is bool, which is why the type of the variable Interactive is bool as well. As for the value of the result, in this case the items being compared are the addresses of the variables cin and is. If you look back at the header for operator >>, you'll see that is is a reference argument that is actually just another name for the istream being supplied as the left-hand argument to the operator >> function. Therefore, what we are testing is whether the istream that is the left-hand argument to operator >> has the same address as cin has (i.e., whether they are different names for the same variable); if so, we are reading data from the keyboard and need to let the user know what we want. Otherwise, we assume the data is from a file, so there is no need to prompt the user.
You may wonder why we have to compare the addresses of these two variables and not simply their contents - that is, why we have to write (&is == &cin) instead of (is == cin). The reason is that the expression (is == cin) compares whether the two streams is and cin are in the same "state"; i.e., whether both (or neither) are available for use, not whether they are the same stream. If this isn't obvious to you, you're not alone. Not only did I not figure it out right away but when I asked a C++ programmer with over 25 years of experience in the language, he took two tries to decipher it.
Once that bit of code is clear, the rest of the changes should be pretty obvious, as they consist of the code needed to display prompts when necessary. For example, the sequence:
cout << "Type (Basic, Music) ";
writes the line "Type (Basic, Music)" to the screen if and only if the input is from cin - that is, if the user is typing at the keyboard. The other similar sequences do the same thing for the other data items that need to be typed in.
Susan still wasn't convinced of the necessity to rewrite operator >>, but I think I won her over.
Susan: But why should we have to change operator >> in the first place? Why not just write a separate function to read the data from the keyboard and leave operator >> as it was? Isn't object-oriented programming designed to allow us to reuse existing functions rather than modify them?
Steve: Yes, but it's also important to try to minimize the number of functions that perform essentially the same operation for the same data type. In the current case, my first impulse was to write a separate function so I wouldn't have to add all those if statements to operator >>. However, I changed that plan when I realized that such a new function would have to duplicate all of the data input operations in operator >>. This means that I would have to change both operator >> and this new function every time I changed the data for any of the HomeItem classes. Since this would cause a maintenance problem in future updates of this program, I decided that I would just have to put up with the if statements.
Susan: Why would this cause a maintenance problem?
Steve: If we have more than one function that shares the same information, we have to locate all such functions and change them whenever that shared information changes. In this case, the shared information is embodied in the code that reads values from an istream and uses those values to create a HomeItem object. Therefore, if we were to change the information needed to create a HomeItem object, we would have to find and change every function that created such an object. In a large program, just finding the functions that were affected could be a significant task.
Another reason to use the same function for both keyboard and file input is that it makes the program easier to read if we use the same function (e.g., operator >>) for similar operations.
Besides changing operator >>, I've added a couple of new functions to the interface for HomeItem - namely, FormattedDisplay and GetName. The first of these functions, as usual for virtual functions declared in the interface of a polymorphic object, simply uses the virtual function mechanism to call the "real" FormattedDisplay function in the object pointed to by m_Worker. The code in the versions of this function in the HomeItemBasic and HomeItemMusic classes is almost identical to the code for Write, except that it adds an indication of what each piece of data represents. Knowing that, you shouldn't have any trouble following either of these functions, so I'm going to list them without comment in Figures 11.23 and 11.24.
FIGURE 11.23. HomeItemBasic::FormattedDisplay (from code\hmit4.cpp)
void HomeItemBasic::FormattedDisplay(ostream& os)
{
os << "Type: ";
os << GetType() << endl;
os << "Name: ";
os << m_Name << endl;
os << "Purchase price: ";
os << m_PurchasePrice << endl;
os << "Purchase date: ";
os << m_PurchaseDate << endl;
os << "Description: ";
os << m_Description << endl;
os << "Category: ";
os << m_Category << endl;
}
FIGURE 11.24. HomeItemMusic::FormattedDisplay (from code\hmit4.cpp)
void HomeItemMusic::FormattedDisplay(ostream& os)
{
HomeItemBasic::FormattedDisplay(os);
os << "Artist: ";
os << m_Artist << endl;
os << "Tracks: ";
int TrackCount = m_Track.size();
os << TrackCount << endl;
for (short i = 0; i < TrackCount; i ++)
os << m_Track[i] << endl;
}
Now that we've looked at FormattedDisplay, what about GetName? You might think that this is about as simple as a function can get, as its sole purpose is to return the value of the m_Name member variable. We'll see shortly how this function is used in the test program. Before we look at that, though, you should note that this function has a characteristic that we haven't run across before. It is a virtual function implemented in HomeItem and HomeItemBasic but not in HomeItemMusic. Why is this, and how does it work?
Inheriting the Implementation of a virtual Function
The answer is that just as with a non-virtual function, if we don't write a new version for a derived class (in this case, HomeItemMusic), the compiler will assume that we are satisfied with the version in the most recently defined base class (in this case, HomeItemBasic). Since the correct behavior of GetName (returning the value of m_Name) is exactly the same in HomeItemMusic as it is in HomeItemBasic, there's no reason to write a new version of GetName for HomeItemMusic, and therefore we won't.
Figure 11.25 shows how we can use these functions to add an item from the keyboard. After adding the item to the inventory, this test program retrieves its name via the GetName function, uses FindItemByName to look it up by its name, and displays it. Just to make sure our new operator >> still works for file input, this test program also loads the inventory from an input file and displays one of the elements read from that file, as the previous test program did. Making sure we haven't broken something that used to work is called regression testing, and it's a very important part of program maintenance.12
FIGURE 11.25. The test program for adding a HomeItem interactively (hmtst4.cpp)
#include <iostream>
#include <fstream>
#include "Vec.h"
#include "hmit4.h"
#include "hmin4.h"
using namespace std;
int main()
{
ifstream HomeInfo("home3.in");
HomeInventory MyInventory;
HomeItem TempItem;
string Name;
MyInventory.LoadInventory(HomeInfo);
TempItem = MyInventory.AddItem();
Name = TempItem.GetName();
HomeItem test2 = MyInventory.FindItemByName(Name);
cout << endl << "Here is the item you added" << endl;
test2.FormattedDisplay(cout);
HomeItem test1 = MyInventory.FindItemByName("Relish");
cout << endl << "Here is an item from the file" << endl;
test1.FormattedDisplay(cout);
return 0;
}
Now we can add a new item and retrieve it, so what feature should we add next? A good candidate would be a way to make changes to data that we've already entered. We will call this new function of the Inventory class EditItem, to correspond to our AddItem function. Let's look at the new interface of the HomeInventory class, which is shown in Figure 11.26.
FIGURE 11.26. The next version of the interface for HomeInventory (code\hmin5.h)
//hmin5.h
class HomeInventory
{
public:
HomeInventory();
short LoadInventory(std::ifstream& is);
void DumpInventory();
HomeItem FindItemByName(std::string Name);
HomeItem AddItem();
short LocateItemByName(std::string Name);
HomeItem EditItem(std::string Name);
private:
Vec<HomeItem> m_Home;
};
You may have noticed that I've added a couple of other support functions besides the new EditItem function. These are DumpInventory, which just lists all of the elements in the m_Home Vec (useful in debugging the program), and LocateItemByName, which we'll cover in the discussion of EditItem.
Susan had a question about the first of these support functions.
Susan: Why do we want to get rid of items with DumpInventory?
Steve: We don't want to. "Dump" is programming slang for "display without worrying about formatting". In other words, a dump function is one that gives "just the facts".
Figure 11.27 shows the test program for this new version of the home inventory application.
FIGURE 11.27. The next version of the HomeInventory test program (code\hmtst5.cpp)
#include <iostream>
#include <fstream>
#include "Vec.h"
#include "hmit5.h"
#include "hmin5.h"
using namespace std;
int main()
{
ifstream HomeInfo("home3.in");
HomeInventory MyInventory;
HomeItem TempItem;
string Name;
MyInventory.LoadInventory(HomeInfo);
TempItem = MyInventory.FindItemByName("Relish");
cout << endl;
TempItem.Edit();
cout << endl;
TempItem.FormattedDisplay(cout);
cout << endl;
return 0;
}
The test program hasn't gotten much more complicated, as you can see; it loads the inventory, uses the new EditItem function to modify one of the items, and then displays the changed item.
Susan had a couple of good questions about this program and some comments about software development issues.
Susan: What happens if the program can't find the object that it's trying to look up?
Steve: That's a very good question. In the present case, that should never happen because the input file does have a record whose name is "Relish". However, we should handle that possibility anyway by checking whether the returned item is null. We'll do so in a later version of the test program; the discussion of that issue is on page 894.
Susan: I don't see why we need to write a whole new function called LocateItemByName. What's wrong with the one we already have, FindItemByName?
Steve: Because FindItemByName returns a copy of the object but doesn't tell us where it came from in the m_Home Vec. Therefore, if we used that function we wouldn't be able to put the object back when we were finished editing it.
Susan: Why can't we leave the classes alone? It's annoying to have to keep changing them all the time.
Steve: I'm afraid that's the way software development works. Of course, I could make it more realistic by playing the role of a pointy-haired manager hovering over you while you're working.
Susan: No, thanks. I believe you.
Steve: Okay, we'll save that for our future management book, Programmers Are from Neptune, Managers Are from Uranus.
Susan: I can't wait.
After that bit of comic relief, let's take a look at the implementation of the new EditItem function, shown in Figure 11.28.
FIGURE 11.28. The EditItem function of HomeInventory (from code\hmin5.cpp)
HomeItem HomeInventory::EditItem(string Name)
{
short ItemNumber = LocateItemByName(Name);
HomeItem TempItem = m_Home[ItemNumber];
TempItem.Edit();
m_Home[ItemNumber] = TempItem;
return TempItem;
}
As you can see, this function isn't too complicated either. It calls LocateItemByName to find the element number of the Homeitem to be edited, copies that HomeItem from the m_Home Vec into a temporary HomeItem called TempItem, calls the Edit function (which we'll get to shortly) for that temporary HomeItem, and then copies the edited HomeItem back into the same position in the m_Home Vec. If you compare this function with AddItem (Figure 11.19 on page 798), you will notice a couple of differences. First, this function calls LocateItemByName rather than FindItemByName. These two functions are exactly the same, except that LocateItemByName returns the element number of the found HomeItem in the m_Home Vec rather than the HomeItem itself. This allows us to update the m_Home Vec with the edited HomeItem when we are through editing it. Second, the call to Edit in this function is different from the call to NewItem in AddItem because Edit has an object to operate on whereas NewItem had to create a previously nonexistent object. Therefore, Edit is a normal (non-static) member function rather than a static member function like AddItem.
What about the implementation of this new Edit function? I have good news and bad news. The good news: Using it is pretty simple. The bad news: Implementing it led to a fairly extensive revision of the HomeItem classes. I think the results are worth the trouble; hopefully, you will come to the same conclusion when we are done. Let's start with the new interface for the HomeItem class.
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,
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!13
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)
{
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 code\hmit5.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 code\hmit5.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 code\hmit5.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 code\hmit5.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 code\hmit5.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 code\hmit5.cpp)
bool HomeItemBasic::EditField(short FieldNumber)
cout << GetFieldName(FieldNumber) << ": ";
cout << GetFieldName(FieldNumber) << ": ";
cout << GetFieldName(FieldNumber) << ": ";
cout << GetFieldName(FieldNumber) << ": ";
cout << GetFieldName(FieldNumber) << ": ";
cout << "Sorry, that is not a valid field number." << endl;
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
11.9. The New Member Functions of HomeItemMusic
We've now covered all the new member functions in HomeItemBasic, so it's time to take a look at the functions implemented in HomeItemMusic. Thankfully, there aren't as many of these. For one thing, GetName, Read, and Edit don't have to be overridden in HomeItemMusic because they do most of their work by calling virtual functions anyway. Two more functions in this class, Write and GetType, haven't changed since we first saw them in hmit1.cpp, so we don't have to go over them again. HomeItemMusic::FormattedDisplay, on the other hand, has changed since we examined it in hmit4.cpp, so it would be a good idea to look it over quickly (Figure 11.42).
FIGURE 11.42. HomeItemMusic::FormattedDisplay (from code\hmit5.cpp)
short HomeItemMusic::FormattedDisplay(ostream &os)
{
short FieldNumber = HomeItemBasic::FormattedDisplay(os);
short TrackCount = m_Track.size();
os << FieldNumber << ". ";
os << GetFieldName(FieldNumber) << ": ";
FieldNumber ++;
os << m_Artist << endl;
os << FieldNumber << ". ";
os << GetFieldName(FieldNumber) << ": ";
FieldNumber ++;
os << TrackCount << endl;
for (short i = 0; i < TrackCount; i ++)
{
os << FieldNumber << ". ";
os << GetFieldName(FieldNumber) << i + 1 << ": ";
FieldNumber ++;
os << m_Track[i] << endl;
}
return FieldNumber;
}
This function isn't very different from its counterpart in HomeItemBasic, but there are a couple of points worth mentioning. First, of course, it calls HomeItemBasic::FormattedDisplay to display the part of the data contained in the base class part of the object. Then it assigns the return value from that call to a local variable called FieldNumber, which it uses to keep track of the current field it is displaying. Why do we want to use the return value from that function to initialize our current field variable rather than a const value, as we did in the base class?
Keeping Track of the Next Field Number
We do this to reduce the difficulty of maintaining this program. If we use the return value from HomeItemBasic::FormattedDisplay, we won't have to make any changes in the derived class function if the HomeItemBasic class eventually has more data added to it; the starting field number in HomeItemMusic:FormattedDisplay will automatically be the right value as long as the modifications to the HomeItemBasic::FormattedDisplay function have been made correctly. Therefore, we have to make such a change only in one place rather than in two, as we would if we used a const value in the derived class function.
Susan wanted a bit more detail on this issue.
Susan: What kind of change would you make that might mess up the field numbers?
Steve: Let's suppose that we had six fields in the HomeItemBasic class, which of course would be numbered 1 through 6. In that case, the added fields in HomeItemMusic would start at 7. However, if we added another field to the HomeItemBasic class, then the number of the first field in the HomeItemMusic class would change to 8. All of this would have to be handled manually if we used a constant value to specify where we wanted to start in the HomeItemMusic class. However, as long as we use the return value from the HomeItemBasic version of FormattedDisplay, any such adjustments will happen automatically.
Susan: But the user might get confused if the field that used to be #5 suddenly became #6.
Steve: True, but there isn't much we can do about that, assuming that the new field was really necessary. All we can do is make sure that the numbers will still be in the right order with no gaps.
I should also mention that the field name prompt for track names is "Track #" followed by the track number. Because we don't want to confuse the user by starting at 0, we add 1 to the value of the loop index before we use it to construct the field name prompt. This ensures that the first track number displayed is 1, not 0. Remember, users don't normally count from 0, and we should humor them; without them, we wouldn't have anyone to use our programs!
Now let's take a look at HomeItemMusic::ReadInteractive (Figure 11.43).
FIGURE 11.43. HomeItemMusic::ReadInteractive (from code\hmit5.cpp)
short HomeItemMusic::ReadInteractive()
{
short TrackCount;
short FieldNumber = HomeItemBasic::ReadInteractive();
cout << FieldNumber << ". ";
cout << GetFieldName(FieldNumber) << ": ";
FieldNumber ++;
getline(cin,m_Artist);
cout << FieldNumber << ". ";
cout << GetFieldName(FieldNumber) << ": ";
FieldNumber ++;
cin >> TrackCount;
m_Track.resize(TrackCount);
for (short i = 0; i < TrackCount; i ++)
{
cout << FieldNumber << ". ";
cout << GetFieldName(FieldNumber) << i + 1 << ": ";
FieldNumber ++;
getline(cin,m_Track[i]);
}
return FieldNumber;
}
There are two more functions that we need to look at briefly. The first is HomeItemMusic::ReadFromFile, which is shown in Figure 11.44. Assuming that you understand the HomeItemBasic versions of ReadFromFile and ReadInteractive, this function should hold no secrets for you, so just take a look at it and make sure you understand it. Then let's move on.
FIGURE 11.44. HomeItemMusic::ReadFromFile (from code\hmit5.cpp)
short HomeItemMusic::ReadFromFile(istream& is)
{
short TrackCount;
HomeItemBasic::ReadFromFile(is);
getline(is,m_Artist);
is >> TrackCount;
is.ignore();
m_Track.resize(TrackCount);
for (short i = 0; i < TrackCount; i ++)
{
getline(is,m_Track[i]);
}
return 0;
}
Finally, there's HomeItemMusic::EditField (Figure 11.45), which has a few points that we should consider before (finally!) ending this chapter.
FIGURE 11.45. HomeItemMusic::EditField (from code\hmit5.cpp)
bool HomeItemMusic::EditField(short FieldNumber)
{
if (FieldNumber < e_Artist)
{
return HomeItemBasic::EditField(FieldNumber);
}
short TrackCount = m_Track.size();
switch (FieldNumber)
{
case e_Artist:
cout << FieldNumber << ". ";
cout << GetFieldName(FieldNumber) << ": ";
getline(cin,m_Artist);
return true;
case e_TrackCount:
cout << FieldNumber << ". ";
cout << GetFieldName(FieldNumber) << ": ";
cin >> TrackCount;
m_Track.resize(TrackCount);
return true;
}
if (FieldNumber > (e_TrackCount + TrackCount))
{
cout << "Sorry, that is not a valid field number." << endl;
return false;
}
cout << FieldNumber << ". ";
cout << GetFieldName(FieldNumber);
cout << FieldNumber - e_TrackCount << ": ";
getline(cin,m_Track[FieldNumber - e_TrackNumber]);
return true;
}
Let's start at the beginning, where we figure out whether the field the user wants to edit is handled by this function or by HomeItemBasic::Edit. If the field number is less than e_Artist, we know that this function isn't responsible for editing it, so we pass the editing task on to the HomeItemBasic version of EditField and return the return value from that function to our caller.
But suppose we have to handle the editing chore for the user's field here. In that case, we need to execute the proper code for the field the user wants to edit. If that field happens to be either the artist's name or the number of tracks, we handle it in the switch statement and return the value true to indicate success. However, handling the other fields (i.e., the track names) isn't quite as simple. If you compare HomeItemBasic::EditField (Figure 11.41) with the current function you'll notice that the switch statement in HomeItemBasic::EditField has a default case to handle the possibility that the field number is invalid, whereas the HomeItemMusic switch statement doesn't. Why is this?
Because a HomeItemMusic object can contain a variable number of track names in its m_Track member variable. This means that we can't tell at compile time how many fields are in the object we're going to edit, which in turn means that we have to wait until run time to figure out whether a particular field number is valid for a particular object. That's the purpose of the if statement
if (FieldNumber > (e_TrackCount + TrackCount))
that adds the number of tracks to the field number for the track count itself and then compares the result to the field number the user typed in. Since the track name fields immediately follow the track count field, if the user's field number is greater than that total, the field number is invalid. For example, if there's only one track, the maximum field number is e_TrackCount + 1; if there are two tracks, it is e_TrackCount + 2; and so on. If the field number the user typed is beyond the legal range, the code in the if statement displays a warning and returns the value false to the calling function to indicate that it was unable to update the object.
Assuming that the user typed in a legal field number, we continue with the code that prompts the user to type in the new name for the selected track:
cout << GetFieldName(FieldNumber);
cout << FieldNumber - e_TrackCount << ": ";
This starts out by displaying the field number for the track to be edited followed by its field name ("Track #" followed by its track number). For example, if the starting field number for the track names is 8, the prompt for track 5 is "12. Track #5: ". Then the line
getline(cin,m_Track[FieldNumber - e_TrackNumber]);
accepts the new value of the track name and stores it in the correct place in the m_Track member variable. Finally, the function returns true to indicate success.
11.10. Review
We started out this chapter by considering the problem of keeping track of all the "stuff" that we accumulate in our homes. This happens to make a good project to illustrate the use of the C++ tools that we've already covered, as well as to introduce a few new tools along the way. It also has the fortunate characteristic that the finished program might actually be useful.
Once we selected a project, the next step was to figure out exactly what the program should do.15 In this case, we wanted to be able to keep track of the type of object, date of acquisition, price, and description for every item in the home, as well as any additional information needed for specific types of objects. An example is the CD, for which the additional information is the name of the artist and those of the tracks.
This led to the more general question of how to divide the nearly infinite number of real-world objects that we might encounter into the categories represented by a necessarily limited number of possible types of program objects. For example, what should we do with LPs or cassette tapes? Because they are used for much the same purpose as music CDs are, I decided to put all of these objects in the same "Music" type. A CD-ROM, on the other hand, is probably best represented by a "software" type, even though it is physically identical to a music CD, because it is used differently and has different information associated with it. For example, a CD-ROM often has a serial number whereas a music CD doesn't.
Having made a decision on this matter, we were ready to start designing the interface that the user of the HomeItem classes will see, using the manager/worker idiom to implement a polymorphic object solution to our problem.
Once we looked at the first version of the interface for the HomeItem class, the most obvious observation was that it is very similar to the StockItem class from previous chapters. The main reason for that similarity is that both of these classes play the role of the manager class in a polymorphic object implementation. One manager class looks very much like any other because a manager object's main duties are to create, destroy, and otherwise handle its worker objects; those duties are similar no matter what the worker objects actually do.
For this reason, all of the "structural" functions in the manager classes for the StockItem and HomeItem types are almost identical except for the exact data types of arguments and return values. These structural functions include the operators << and >>, the default constructors, copy constructors, "normal" constructors, assignment operators, and destructors. They also include the "special" constructor used to prevent an infinite loop during construction of a worker object and the Write function used to create a human-readable version of the data for an object.
By contrast, the "regular" member functions of each of these manager classes are whatever is needed to allow the users of that class to get their work done. These functions differ significantly from one polymorphic object implementation to another, as they are the most specific to the particular task of each class.
Upon examination, we saw that the same analogies apply to the worker classes for these two types as to the manager classes. The main difference between the StockItem worker classes and the first version of the HomeItem worker classes is that the HomeItem worker classes have a GetType member function that the StockItem worker classes don't have. This function allows the base class member data to be displayed with the correct tag indicating the exact type of the worker object being displayed, rather than requiring the duplication of code in the base and derived class output functions.16
Because this GetType function is used only within the implementation of the worker classes, it is first declared in HomeItemBasic rather than in HomeItem. This doesn't violate the principle that all objects of a polymorphic object type must have the same user interface, because GetType is not visible to the user of the HomeItem types and therefore doesn't change the user's view of these objects.
Next, we analyzed the first version of operator >> for the HomeItem classes. This function differs from the StockItem version of the same operator in a number of ways, the primary differences being its ability to skip blank lines in the input file, its deferred creation of variables until they are needed rather than all at the beginning of the function, and its use of an explicit type indicator ("Basic" or "Music") to determine the type of the worker object, rather than relying on the value of the expiration date field as the operator >> for StockItem did.
The most generally applicable of these differences is the deferred creation of variables, which is a good way to save execution time and make the program easier to follow. In this case, we didn't create a Vec of track names until we had read the rest of the data for the object, including its type and track count; of course, a "Basic" object doesn't have the track count and track name fields anyway.
This implementation of operator >> also illustrates the new C++ feature that restricts the scope of a for index variable created in a for loop header to the controlled block of that for statement. Earlier versions of C++ allowed that variable to be accessed after the end of the for loop, an oversight that was corrected in the final C++ standard.
Then we moved on to the HomeItemBasic::Write function, which uses the GetType function to determine which type of object it is writing to the ostream specified by its argument. The HomeItemBasic version of Write has to call GetType because the derived class function HomeItemMusic::Write calls this function to do most of the work; thus, the object for which HomeItemBasic::Write has been called may be a derived class object rather than a HomeItemBasic object. This means that HomeItemBasic::Write has to call the virtual function GetType to find out the actual type of the object being written, so that it can write the correct type indicator to the output file along with the other data for the object. This is what makes it possible for us to reconstruct the object properly when we read the data back from the file.
When we got done with HomeItemBasic::Write, we continued by analyzing HomeItemMusic::Write. That function is pretty simple, except that it uses the size member function of the Vec data type to find out how many tracks are in the Vec so that it can write that track count, followed by all of the track names. This is necessary for us to reconstruct the "Music" object properly when we read its data from the file.
The next operation we undertook was to create a new HomeInventory class, which serves much the same function for HomeItems that the Inventory class does for StockItems; it allows us to create and keep track of a number of HomeItems and to search for a particular HomeItem.
The initial interface of this new HomeInventory class is also pretty simple, providing only the minimal set of operations we might want to use: loading the inventory from a disk file and searching for a particular item by name. To keep track of the number of elements in use, we decided to store the number of elements in the input file and creating a Vec of the correct size when we open the file. If we add new items to the Vec, we keep track of that fact via its size member function.
This solution eliminates the waste of time caused by resizing the Vec while reading data from the input file, but it also has a twist of its own: the need to read the data into a temporary holding variable so we can detect the end of the input file without running off the end of the Vec. This problem led to a discussion of the dangers of ignoring the possibility of errors in the data, as well as other sources of errors that lie outside the immediate scope of the code we write.
We continued by creating the ability for the user to enter data for a new object by adding an AddItem member function to the HomeInventory class and a NewItem static member function to the HomeItem class. The latter needs to be a static member function because it creates a new HomeItem object by reading data from the keyboard; it doesn't have an existing HomeItem object to work on, so it can't be a regular member function.
The implementation of HomeItem::NewItem is quite simple: it uses operator >> to read data from the keyboard into a newly created HomeItem object and then returns it to the caller. However, this works only because we changed operator >> to be usable for keyboard input by having it determine whether the user is typing at the keyboard. We do this by testing whether the input stream is cin. If so, the function displays prompts before each input operation; if not, the input operations are performed as they were previously.
We had to change operator >> rather than using it as previously implemented because adding another function for keyboard input would have required us to duplicate the code that reads the data, causing maintenance problems when any of those input operations had to be modified.
Then we added versions of FormattedDisplay to both HomeItemBasic and HomeItemMusic, which provide output virtually identical to the output of operator << for objects of those two types, except that FormattedDisplay also displays labels indicating what the data items represent.
The next order of business was to discuss GetName, whose only notable characteristic is that even though it is a virtual function, it is not implemented in HomeItemMusic because the implementation in HomeItemBasic will work perfectly for a HomeItemMusic object. As with a non-virtual function, if we call a virtual function for a derived class object that hasn't been defined in that class, the result will be to call the function in the nearest base class that defines the function - in this case HomeItemBasic::GetName.
Next we analyzed the test program hmtst4.cpp, which includes the new AddItem and GetName functions added since the last test program. We used these functions to add a new item and retrieve it, as well as to retrieve an item loaded from the file (just to make sure that still worked after all the changes we'd made).
In the next step, we added a mechanism to edit an object that already exists, via a new function called EditItem in the HomeInventory class and a corresponding function called Edit in the HomeItem class. We also added a helper function called LocateItemByName to the HomeInventory class to help us find an existing HomeItem to be edited.
We didn't have to change the test program very much from the previous version to accommodate this new ability to edit an existing object. However, we did have to modify the HomeItem classes significantly so that we could avoid keeping track of the field names in more than one function; this was intended to simplify maintenance later. One modification was the addition of a Read function that could fill in the data for an existing object rather than create the object directly in operator >>, as we had done until that point.
This revamping of the input mechanism required several new functions, which I added to HomeItemBasic and HomeItemMusic rather than to HomeItem, because they are used only as aids to the implementation of the new Read function. Included among these functions are ReadInteractive, ReadFromFile, EditField, and GetFieldName.
The last of these, GetFieldName, is particularly interesting on several counts. First, it uses a new construct, the enum, which is a way to define a number of constant values that are appropriate for naming array or Vec indexes. Second, it uses a static array of strings to hold the field names for use in prompts. This array is static so that it will be initialized only once, during the first call to GetFieldName, rather than every time the function is called. The reason we're using an array instead of a Vec is that it is (unfortunately) impossible in C++ to initialize a Vec from a list of values; this special C++ facility is available only for arrays. Because we're using an array, we have to check for the possibility of a bad index rather than rely on the safety checks built into a Vec; the program would fail in some mysterious way if we tried to access a nonexistent array element.
After some discussion of the properties of enums, including their ability to be converted to an integer type automatically when needed for arithmetic purposes, we continued with the implementation of Read, which merely decides whether the input is interactive and then calls the appropriate subfunction, ReadInteractive or ReadFromFile, accordingly. ReadInteractive, as its name implies, prompts the user for each field to be entered, using GetFieldName to retrieve the appropriate prompt for that field. ReadFromFile reads the same data but without displaying any prompts.
The other new function added to the HomeItem interface, Edit, first calls the updated version of FormattedDisplay to display the current data for the object to be edited, using GetFieldName to determine the prompt for each field to be displayed, and then calls EditField to request the new value for the field being modified. To simplify the code, this function uses a new construct, switch. This is essentially equivalent to a number of if and else statements that select one of a number of possible actions.
After finishing the changes to HomeItemBasic, we examined the corresponding functions in HomeItemMusic, which use the HomeItemBasic base class functions to do as much of the work as possible. For this reason, the new HomeItemMusic functions added no great complexity except for the necessity of handling a variable number of data elements in the track name Vec.
11.11. Exercises
1. Implement the HomeItemComputer class as a derived class of HomeItemBasic to keep track of computers. The added fields should include serial number, amount of RAM, amount of disk space, a list of installed storage devices, and lists of installed ISA and PCI interface cards.
2. Implement the HomeItemSoftware class as a derived class of HomeItemBasic to keep track of computer software. The added fields should include the serial numbers of the software and computer on which it is installed. Can you devise a way to make sure that the latter serial number is the same as the serial number of a HomeItemComputer in the inventory?
3. Implement the HomeItemAppliance class as a derived class of HomeItemBasic to keep track of other electric and electronic appliances. The added fields should include the serial number of the appliance.
4. Can you think of a way to simplify the implementation of the classes in the above exercises by adding an additional class?
5. Implement the HomeItemBook class as a derived class of HomeItemBasic to keep track of books. The added fields should include author, publisher, publication date, number of pages, and ISBN (International Standard Book Number, a 10-character field that can contain the digits from 0 to 9 and the letter X).
6. Implement the HomeItemSet class as a derived class of HomeItemBasic to keep track of sets of identical items such as plates and flatware. The added fields should include the pattern name and number of items of each type.
7. Implement the HomeItemClothing class as a derived class of HomeItemBasic to keep track of clothes and shoes. The added fields should include owner's name and size.
8. Add code to the AddItem member function of the HomeInventory class to make sure that the new object being added has a name different from that of every object already in the inventory.
11.12. Conclusion
At this point, we have a working, if rudimentary, set of HomeItem classes along with the HomeInventory class that manages the objects of these classes. Next, we're going to add some more facilities to the string class so that we can improve our application program enough to actually use it for keeping track of all the "stuff" we collect in everyday life.
1 Of course, if you still have 8-track tapes, you probably also need a "lava lamp" category.
2 This is an oversimplification because both music CDs and CD-ROMs can be stored in the same "CD holder", whereas cassettes and LPs each need their own type of holders. Therefore, for the purpose of figuring out how much physical storage space you need for each type of object, the physical form of the object is indeed important. As always, the question is how you will use the information, not merely what information is available.
3 As we'll see, HomeItem will eventually have its own version of GetName. Many classes need the ability to retrieve the name of an item; using a function called something like GetName is a fairly common way to handle this requirement.
4 See the Glossary for details on the long and double data types.
5 It took quite a few compiles before I actually had an executable to run, but that was mostly because I started writing this chapter and the HomeItem program on my laptop while on a trip away from home. Because I had a relatively small screen to work on and no printer, it was faster to use the compiler to tell me about statements that I needed to change.
6 Unfortunately, as of this writing, not all compilers support this new feature. If you want your programs to compile under both the old and new rules, you will have to define your loop variables outside the loop headers.
7 Actually, there is a different size function for each different type of Vec - Vecs of strings, Vecs of shorts, Vecs of StockItems, and so on, all have their own size member functions. However, this is handled automatically by the compiler, so it doesn't affect our use of the size function.
8 I'm oversimplifying here. There is a way to do this without a separate class: we could create a static member function of HomeItem that would find a specific HomeItem. But this would be a bad idea, because it would prevent us from ever having more than one set of HomeItems in a given program. That's because static member functions apply to all items in a given class, not merely a particular set of items such as we can manage with a separate inventory class.
9 For the moment, I'm going to assume that each name that the user types in for a new object is unique. We'll add code to check this in one of the exercises.
10 In fact, the implementation of the vector type underlying our Vec type may (and almost certainly does) work more efficiently than this, but we shouldn't write our programs in an extremely inefficient way and hope that the standard library implementers can make up for that inefficiency. That's just sloppy programming.
11 By an amazing coincidence, we actually do have a cat named Bonsai.
12 In fact, operator >> did not work correctly for file input the first time I tried it because I had made the mistake of testing the equality of the istreams is and cin rather than the equality of their addresses, as mentioned previously. So it's a good thing I thought to check that use of operator >>!
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.
14 That discussion starts on page 455.
15 This may seem too simple even to mention, but the failure to define exactly what a particular program is supposed to do is a major cause of wasted money and effort, especially in large corporations.
16 The use of a GetType member function to allow sharing of code for the base class member data display would probably have been a good idea for the StockItem classes as well, but I hadn't thought of it yet when I designed those classes.