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]

[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.

The Initial HomeItem Test Program

Figure 11.3. The initial test program for the HomeItem classes (codehmtst1.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 692, 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 (codehmit1.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 (codehmit1.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 codehmit1.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 codehmit1.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 >>
while (Type == "")
 {
 getline(is,Type);
 }

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:

is >> TrackCount;
is.ignore();
Vec<string> Track(TrackCount);
for (short i = 0; i < TrackCount; i ++)
  {
  getline(is,Track[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]

[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.

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 (codefortest.cpp)
#include <iostream>
using namespace std;

int main()
{
  for (short i = 0; i < 10; i ++)
   {
   cout << i << endl;
   }

  cout << endl;

  for (short i = 0; i < 10; i ++)
   {
   cout << 2 * i << endl;
   }
}

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()
: HomeItem(1),
  m_Name(),
  m_PurchasePrice(0),
  m_PurchaseDate(0),
  m_Description(0),
  m_Category(0)
{
}

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 codehmit1.cpp)
string HomeItemBasic::GetType()
{
 return "Basic";
}

Figure 11.12. HomeItemMusic::GetType (from codehmit1.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 codehmit1.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 codehmit1.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]

[7] Actually, there is a different size function for each different type of VecVecs 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.

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]

[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.

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.

[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.

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 (codehmin2.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 (codehmin2.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

if (is.fail() != 0)
   break;

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

m_Home.resize(i);
return i;

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]

[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.

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.

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

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