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.
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: 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 first statement of this function, HomeItem TempItem = HomeItem::NewItem();, creates a new HomeItem object called TempItem.
- 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.1
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.
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.
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.
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: 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.
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:
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: 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.
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.2
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.
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: 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".