6.10. Reference Arguments
As you may recall from Chapter 5, when we call a function, the arguments to that function are actually copies of the data supplied by the calling function; that is, a new local variable is created and initialized to the value of each expression from the calling function and the called function works on that local variable. Such a local variable is called a value argument, because it is a new variable with the same value as the caller's original argument. There's nothing wrong with this in many cases, but sometimes, as in the present situation, we have to do it a bit differently. A reference argument, such as the istream& argument to Read, is not a copy of the caller's argument, but another name for the actual argument passed by the caller.
Reference arguments are often more efficient than value arguments, because the overhead of making a copy for the called function is avoided. Another difference between value arguments and reference arguments is that any changes made to a reference argument change the caller's actual argument as well, which in turn means that the caller's actual argument must be a variable, not an expression like x + 3; changing the value of such an expression wouldn't make much sense. This characteristic of reference arguments can confuse the readers of the calling function; there's no way to tell just by looking at the calling function that some of its variables can be changed by calling another function. This means that we should limit the use of reference arguments to those cases where they are necessary.
In this case, however, it is necessary to change the stream object that is the actual argument to the Read function, because that object contains the information about what data we've already read from the stream. If we passed the stream as a value argument, then the internal state of the "real" stream in the calling function wouldn't be altered to reflect the data we've read in our Read function, so every time we called Read, we would get the same input again. Therefore, we have to pass the stream as a reference argument.
The complete decoding of the function declaration void StockItem::Read(istream& s) is shown in Figure 6.14. Putting it all together: we're defining a void function (one that doesn't return a value), called Read, which belongs to class StockItem. This function takes an argument named s that's a reference to an istream. That is, the argument s is another name for the istream passed to us by the caller, not a copy of the caller's istream.
- Susan: How does Read make Shopinfo go get data?
- Steve: Well, the argument s is a reference to the stream object provided by the caller; in this case, Shopinfo. That stream is connected to the file shop2.in.
- Susan: Ok, but in the test program, Shopinfo, which is an ifstream, is passed as an argument to the Read member function in the StockItem class. But Read(istream&) function in the StockItem class expects a reference to an istream as an argument. I understand the code of both functions, but I don't see how you can pass an ifstream as an istream. As far as I know, we use fstreams to write to and read from files and istream and ostream to read from the keyboard and write to the screen. So, can you mix these when you pass them on as arguments?
- Steve: Yes. As we'll see later, this is legal because of the relationship between the ifstream and istream classes.1
- Susan: How does Read do the reading? How come you are using >> without a cin statement?
- Steve: cin isn't a statement, but a stream that is created automatically whenever we #include <iostream> in our source file. Therefore, we can read from it without connecting it to a file. In this case, we're reading from a different stream, namely s.
- Susan: How come this is a void type? I would think it would return data being read from a file.
- Steve: You would think so, wouldn't you? I love it when you're logical. However, what it actually does is to read data from a file into the object for which it was called. Therefore, it doesn't need a return value.
- Susan: So the ifstream object is a transfer mechanism? That is, ifstream s; would read data from a file named s?
- Steve: Yes, ifstream is a transfer mechanism. However, ifstream s; would create an ifstream called s that was not connected to any file; the file could be specified later. If we wanted to create an ifstream called s that was connected to a file called xyz, then we would write ifstream s("xyz");.
- Susan: OK. An ifstream just reads data from a file. It doesn't care which file, until you specify it?
- Steve: Right.
- Susan: What does this mean without cin? Is it just the same thing, only you can't call it cin because cin is for native use and this is a class? How come the >> is preceded by the argument s?
- Steve: The s takes the place of cin, because we want to read from the stream s, not the stream cin, which is a stream that is connected to the keyboard. Whatever is typed on the keyboard goes into cin, whereas the source for other streams depends on how they are set up. For example, in this program, we have connected the stream called s to a file called "shop2.in".
- Susan: Tell me what you mean by "just a stream".
- Steve: Think of it like a real stream, with the bytes as little barges that float downstream. Isn't that poetic? Anyway, there are three predefined streams that we get automatically when we #include <iostream>: cin, cout, and cerr. The first two we've already seen, and the last one is intended for use in displaying error messages.
There is one point that we haven't examined yet, though, which is how this routine determines that it's finished reading from the input file. With keyboard input, we process each line separately after the user hits Enter, but that won't do the job with a file, where we want to read all the items in until we get to the end of the file. We actually handle this detail in the main program itemtst2.cpp (Figure 6.11 on page 345) by asking ShopInfo whether there is any data left in the file; to be more precise, we call the ifstream member function fail() to ask the ShopInfo ifstream whether we have tried to read past the end of the file. If we have, then the result of that call to ShopInfo.fail() will be nonzero (which signifies true). If we haven't yet tried to read past the end of the file, then the result of that call will be 0 (which signifies false). How do we use this information?
We use it to decide whether to execute a break statement. This is a loop control device that interrupts processing of a loop whenever it is executed. The flow of control passes to the next statement after the end of the controlled block of the for statement.2
The loop will terminate in one of two ways. Either 100 records have been read, in which case i will be 100; or the end of the file is reached, in which case i is the number of records that have been read successfully.
- Susan: What is fail()?
- Steve: It's a member function of the ifstream class.
- Susan: Where did it come from?
- Steve: From the standard library.
- Susan: But it could be used in other classes, right?
- Steve: Not unless they define it as well.3
- Susan: How does all the data having been read translate into "nonzero"? What makes a "nonzero" value true?
- Steve: That's a convention used by that function.
- Susan: So anything other than 0 is considered true?
- Steve: Yes.
- Susan: Where did break come from?
- Steve: It's another keyword like for; it means to terminate the loop that is in progress.
- Susan: I do not understand what is actually happening with the program at this time. When is break implemented? Is it just to end the reading of the entire file?
- Steve: We have to stop reading data when there is no more data in the file. The break statement allows us to terminate the loop when that occurs.
- Susan: What do you mean that the loop will terminate either by 100 records being read or when the end of the file is reached? Isn't that the same thing?
- Steve: It's the same thing only if there are exactly 100 records in the file.
- Susan: So you mean when there are no more records to be read? So that the loop won't continue on till the end with nothing to do?
- Steve: Exactly.
- Susan: So does i just represent the number of records in the file?
- Steve: Actually, it's the number of records that we've read.
- Susan: Does this library have a card catalogue? I would like to know what else is in there.
- Steve: There is a library reference manual for most libraries. If you get a library with a commercial compiler, that manual comes with the compiler documentation; otherwise, it's usually an on-line reference (that is, a help file). There's also quite a good book about the C++ standard library, with the very imaginative name The C++ Standard Library, written by Nicolai Josuttis (ISBN 0-201-37926-0). Every serious C++ programmer should have a copy of that book.
- Susan: A novice would not know this. Put it in the book.
- Steve: Done.
- Susan: Well, the program sounded like that indeed there were 100 records in the file. However, I see that in practice that might change, and why you would therefore need to have a break.
- Steve: You obviously understand this.
Whether there are 100 records in the file or fewer than that number, obviously the number of items in the Vec is equal to the current value of i. Or is it?
Fencepost Errors
Let's examine this a bit more closely. You might be surprised at how easy it is to make a mistake in counting objects when writing a program. The most common error of this type is thinking you have one more or one less than the actual number of objects. In fact, this error is common enough to have a couple of widely known nicknames: off by one error and fencepost error. The former name should be fairly evident, but the latter name may require some explanation. First, let's try it as a "word problem". If you have to put up a fence 100 feet long and each section of the fence is 10 feet long, how many sections of fence do you need? Obviously, the answer is 10. Now, how many fenceposts do you need? 11. The confusion caused by counting fenceposts when you should be counting segments of the fence (or vice versa) is the cause of a fencepost error.
That's fine as a general rule, but what about the specific example of counting records in our file? Well, let's start out by supposing that we have an empty file, so the sequence of events in the first loop in the current main program (Figure 6.11 on page 345) is as follows:
- 1. Set i to 0.
- 2. Is i less than 100? If not, exit. If so, continue.
- 3. Use the Read function to try to read a record into the ith element of the AllItems Vec.
- 4. Call ShopInfo.fail() to find out whether we've tried to read past the end of the file.
- 5. If so, execute the break statement to exit the loop.
The answer to the question in step 4 is that in fact nothing was read, so we do execute the break and leave the loop. The value of i is clearly 0 here, because we never went back to the top of the loop; since we haven't read any records, setting InventoryCount to i works in this case.
Now let's try the same thing, but this time assuming that there is one record in the file. Here's the sequence of events:
- 1. Set i to 0.
- 2. Is i less than 100? If not, exit. If so, continue.
- 3. Use the Read function to try to read a record into the ith element of the AllItems Vec.
- 4. Call ShopInfo.fail() to find out whether we've tried to read past the end of the file.
- 5. If so, execute the break statement to exit the loop. In this case, we haven't run off the end of the file, so we go back to the top of the loop, and continue as follows:
- 6. Increment i to 1.
- 7. Is i less than 100? If not, exit. If so, continue.
- 8. Call Read to try to read a record into the AllItems Vec.
- 9. Call ShopInfo.fail() to find out whether we've tried to read past the end of the file.
- 10. If so, execute the break statement to exit the loop.
The second time through, we execute the break. Since i is 1, and the number of elements read was also 1, it's correct to set the count of elements to i.
It should be pretty clear that this same logic applies to all the possible numbers of elements up to 99. But what if we have 100 elements in the file? Relax, I'm not going to go through these steps 100 times, but I think we should start out from the situation that would exist after reading 99 elements and see if we get the right answer in this case, too. After the 99th element has been read, i will be 99; we know this from our previous analysis that indicates that whenever we start executing the statements in the controlled block of the loop, i is always equal to the number of elements previously read. So here's the 100th iteration of the loop:
- 1. Call Read to try to read a record into the AllItems array.
- 2. Call ShopInfo.fail() to find out whether we've tried to read past the end of the file.
- 3. If so, execute the break statement to exit the loop.
- 4. Otherwise, increment i to 100.
- 5. Is i less than 100? If not, exit. If so, continue.
- 6. Since i is not less than 100, we exit.
At this point, we've read 100 records and i is 100, so these two numbers are still the same. Therefore, we can conclude that setting InventoryCount equal to i when the loop is finished is correct; we have no fencepost error here.
- Susan: Why are you always saying that "it's correct to set the count of elements to i"?
- Steve: Because I'm showing how to tell whether or not we have a fencepost error. That requires a lot of analysis.
Actually, this whole procedure we've just been through reminds me of the professor who claimed that some point he was making was obvious. This was questioned by a student, so the professor spent 10 minutes absorbed in calculation and finally emerged triumphantly with the news that it was indeed obvious.
Assuming that you've installed the software from the CD in the back of this book, you can try out this program. First, you have to compile it by following the compilation instructions on the CD. Then type itemtst2 to run the program. You'll see that it indeed prints out the information from the StockItem objects. You can also run it under the debugger by following the usual instructions for that method.
Other Possible Transactions with the Inventory
Of course, this isn't all we want to do with the items in the store's inventory. Since we have a working means of reading and displaying the items, let's see what else we might want to do with them. Here are a few possible transactions at the grocery store:
- 1. George comes in and buys 3 bags of marshmallows. We have to adjust the inventory for the sale.
- 2. Sam wants to know the price of a can of string beans.
- 3. Judy comes in looking for chunky chicken soup; there's none on the shelf where it should be, so we have to check the inventory to see if we're supposed to have any.
All of these scenarios require the ability to find a StockItem object given some information about it. Let's start with the first example, which we might state as a programming task in the following manner: "Given the UPC from the bag of marshmallows and the number of bags purchased, adjust the inventory by subtracting the number purchased from the previous quantity on hand." Figure 6.15 is a program intended to solve this problem.
Here is a more detailed analysis of the steps that the program in Figure 6.15 is intended to perform:
- 1. Take the UPC from the item.
- 2. For every item in the inventory list, check whether its UPC is the same as the one from the item.
- 3. If it doesn't match, go back to step 2.
- 4. If it does match, subtract the number purchased from the inventory.
There's nothing really new here except for the bool variable type, which we'll get to in a moment, and the -= operator that the program uses to adjust the inventory. -= is just like +=, except that it subtracts the right-hand value from the left-hand variable, while += adds.
The bool variable type is a relatively new addition to C++ that was added to C++ in the process of developing the standard and is available on any compiler that conforms to the standard. Expressions and variables of this type are limited to the two values true and false.4 We've been using the terms true and false to refer to the result of a logical expression such as if (x < y); similarly, a bool variable or function return value can be either true or false.
Attempting to Access private Member Variables
If you compile the program in Figure 6.15, you'll find that it is not valid. The problem is the lines:
- "if the input UPC is the same as the value of the m_UPC member variable of the object stored in the ith element of the AllItems Vec, then..."
- "subtract the number of items purchased from the value of the m_InStock member variable of the object stored in the ith element of the AllItems Vec".
While both of these lines are quite understandable to the compiler, they are also illegal because they are trying to access private member variables of the StockItem class, namely m_UPC and m_InStock, from function main. Since main is not a member function of StockItem, this is not allowed. The error message from the compiler should look something like Figure 6.16.
Does this mean that we can't accomplish our goal of updating the inventory? Not at all. It merely means that we have to do things "by the book" rather than going in directly and reading or changing member variables that belong to the StockItem class. Of course, we could theoretically "solve" this access problem by simply making these member variables public rather than private. However, this would allow anyone to mess around with the internal variables in our StockItem objects, which would defeat one of the main purposes of using class objects in the first place: that they behave like native types as far as their users are concerned. We want the users of this class to ignore the internal workings of its objects and merely use them according to their externally defined interface; the implementation of the class is our responsibility, not theirs.
This notion of implementation being separated from interface led to an excellent question from Susan:
- Susan: Please explain to me why you needed to list those member variables as private in the interface of StockItem. Actually, why do they even need to be there at all? Well, I guess you are telling the compiler that whenever it sees the member variables that they will always have to be treated privately?
- Steve: They have to be there so that the compiler can figure out how large an object of that class is. Many people, including myself, consider this a flaw in the language design because private variables should really be private, not exposed to the class user.
Obviously, she'd lost her true novice status by this point. Six months after finding out what a compiler is, she was questioning the design decisions made by the inventor of C++; what is more, her objections were quite well founded.
As it happens, we can easily solve our access problem without exposing the implementation of our class to the user any more than it already has been by virtue of the header file. All we have to do is to add a couple of new member functions called CheckUPC and DeductSaleFromInventory to the StockItem class; the first of these allows us to check whether a given UPC belongs to a given StockItem, and the second allows us to adjust the inventory level of an item.
Susan had another suggestion as to how to solve this problem, as well as a question about why I hadn't anticipated it in the first place:
- Susan: Hey, wouldn't it be easier to write a special main that is a member function to get around this?
- Steve: That's an interesting idea, but it wouldn't work. For one thing, main is never a member function; this is reasonable when you consider that you generally have quite a few classes in a program. Which one would main be a member function of?
- Susan: So then all these new member functions do is to act as a gobetween linking the StockItem class and the inventory update program to compare data that is privately held in the StockItem class?
- Steve: Yes, the new entries in the interface are designed to make the private data available in a safe manner. I think that's the same as what you're saying.
- Susan: If you wanted to change the program, why didn't you just do it in the first place instead of breaking it down in parts like this?
- Steve: Because that's not the way it actually happens in real life.
- Susan: Do you think it less confusing to do that, and also does this act as an example of how you can modify a program as you see the need to do it?
- Steve: Right on both counts.
I recommend that you print out the files that contain this interface and its implementation as well as the test program, for reference as you are going through this part of the chapter; those files are item4.h, item4.cpp, and itemtst4.cpp, respectively. The declarations of the two new functions, CheckUPC and DeductSaleFromInventory, should be pretty easy to figure out: CheckUPC takes the UPC that we want to find and compares it to the UPC in its StockItem, then returns true if they match and false if they don't. Here's another good use for the bool data type: the only possible results of the CheckUPC function are that the UPC in the StockItem matches the one we've supplied (in which case we return true) or it doesn't match (in which case we return false). DeductSaleFromInventory takes the number of items sold and subtracts it from the previous inventory. But where did GetInventory and GetName come from?
Making the Program More User-friendly
I added those functions because I noticed that the "itemtst" program wasn't very user-friendly. Originally it followed these steps:
- 1. Ask for the UPC.
- 2. Ask for the number of items purchased.
- 3. Search through the list to see whether the UPC is legitimate.
- 4. If so, adjust the inventory.
- 5. If not, give an error message.
- 6. Exit.
What's wrong with this picture? Well, for one thing, why should the program make me type in the number of items sold if the UPC is no good? Also, it never told me the new inventory or even what the name of the item was. It may have known these things, but it never bothered to inform me. So I changed the program to work as follows:
- 1. Ask for the UPC.
- 2. Search through the list to see whether the UPC was legitimate.
- 3. If not, give an error message and exit.
- 4. If the UPC was OK, then
- 5. Exit.