Section 14.2. Input and Output Operators


14.2. Input and Output Operators

Classes that support I/O ordinarily should do so by using the same interface as defined by the iostream library for the built-in types. Thus, many classes provide overloaded instances of the input and output operators.

14.2.1. Overloading the Output Operator <<

To be consistent with the IO library, the operator should take an ostream& as its first parameter and a reference to a const object of the class type as its second. The operator should return a reference to its ostream parameter.



The general skeleton of an overloaded output operator is

      // general skeleton of the overloaded output operator      ostream&      operator <<(ostream& os, const ClassType &object)      {          // any special logic to prepare object          // actual output of members          os << // ...          // return ostream object          return os;      } 

The first parameter is a reference to an ostream object on which the output will be generated. The ostream is nonconst because writing to the stream changes its state. The parameter is a reference because we cannot copy an ostream object.

The second parameter ordinarily should be a const reference to the class type we want to print. The parameter is a reference to avoid copying the argument. It can be const because (ordinarily) printing an object should not change it. By making the parameter a const reference, we can use a single definition to print const and nonconst objects.

The return type is an ostream reference. Its value is usually the ostream object against which the output operator is applied.

The Sales_item Output Operator

We can now write the Sales_item output operator:

      ostream&      operator<<(ostream& out, const Sales_item& s)      {          out << s.isbn << "\t" << s.units_sold << "\t"              << s.revenue << "\t" << s.avg_price();          return out;      } 

Printing a Sales_item entails printing its three data elements and the computed average sales price. Each element is separated by a tab. After printing the values, the operator returns a reference to the ostream it just wrote.

Output Operators Usually Do Minimal Formatting

Class designers face one significant decision about output: whether and how much formatting to perform.

Generally, output operators should print the contents of the object, with minimal formatting. They should not print a newline.



The output operators for the built-in types do little if any formatting and do not print newlines. Given this treatment for the built-in types, users expect class output operators to behave similarly. By limiting the output operator to printing just the contents of the object, we let the users determine what if any additional formatting to perform. In particular, an output operator should not print a newline. If the operator does print a newline, then users would be unable to print descriptive text along with the object on the same line. By having the output operator perform minimal formatting, we let users control the details of their output.

IO Operators Must Be Nonmember Functions

When we define an input or output operator that conforms to the conventions of the iostream library, we must make it a nonmember operator. Why?

We cannot make the operator a member of our own class. If we did, then the left-hand operand would have to be an object of our class type:

      // if operator<< is a member of Sales_item      Sales_item item;      item << cout; 

This usage is the opposite of the normal way we use output operators defined for other types.

If we want to support normal usage, then the left-hand operand must be of type ostream. That means that if the operator is to be a member of any class, it must be a member of class ostream. However, that class is part of the standard library. Weand anyone else who wants to define IO operatorscan't go adding members to a class in the library.

Instead, if we want to use the overloaded operators to do IO for our types, we must define them as a nonmember functions. IO operators usually read or write the nonpublic data members. As a consequence, classes often make the IO operators friends.

Exercises Section 14.2.1

Exercise 14.7:

Define an output operator for the following CheckoutRecord class:

      class CheckoutRecord {      public:          // ...      private:          double book_id;          string title;          Date date_borrowed;          Date date_due;          pair<string,string> borrower;          vector< pair<string,string>* > wait_list;      }; 

Exercise 14.8:

In the exercises to Section 12.4 (p. 451) you wrote a sketch of one of the following classes:

      (a) Book     (b) Date     (c) Employee      (d) Vehicle  (e) Object   (f) Tree 

Write the output operator for the class you chose.


14.2.2. Overloading the Input Operator >>

Similar to the output operator, the input operator takes a first parameter that is a reference to the stream from which it is to read, and returns a reference to that same stream. Its second parameter is a nonconst reference to the object into which to read. The second parameter must be nonconst because the purpose of an input operator is to read data into this object.

A more important, and less obvious, difference between input and output operators is that input operators must deal with the possibility of errors and end-of-file.



The Sales_item Input Operator

The Sales_item input operator looks like:

      istream&      operator>>(istream& in, Sales_item& s)      {          double price;          in >> s.isbn >> s.units_sold >> price;          // check that the inputs succeeded          if (in)             s.revenue = s.units_sold * price;          else             s = Sales_item(); // input failed: reset object to default state          return in;      } 

This operator reads three values from its istream parameter: a string value, which it stores in the isbn member of its Sales_item parameter; an unsigned, which it stores in the units_sold member; and a double, which it stores in a local named price. Assuming the reads succeed, the operator uses price and units_sold to set the object's revenue member.

Errors During Input

Our Sales_item input operator reads the expected values and checks whether an error occurred. The kinds of errors that might happen include:

  1. Any of the read operations could fail because an incorrect value was provided. For example, after reading isbn, the input operator assumes that the next two items will be numeric data. If nonnumeric data is input, that read and any subsequent use of the stream will fail.

  2. Any of the reads could hit end-of-file or some other error on the input stream.

Rather than checking each read, we check once before using the data we read:

      // check that the inputs succeeded      if (in)          s.revenue = s.units_sold * price;      else          s = Sales_item(); // input failed: reset object to default state 

If one of the reads failed, then price would be uninitialized. Hence, before using price, we check that the input stream is still valid. If it is, we do the calculation and store it in revenue. If there was an error, we do not worry about which input failed. Instead, we reset the entire object as if it were an empty Sales_item. We do so by creating a new, unnamed Sales_item constructed using the default constructor and assigning that value to s. After this assignment, s will have an empty string for its isbn member, and its revenue and units_sold members will be zero.

Handling Input Errors

If an input operator detects that the input failed, it is often a good idea to make sure that the object is in a usable and consistent state. Doing so is particularly important if the object might have been partially written before the error occurred.

For example, in the Sales_item input operator, we might successfully read a new isbn, and then encounter an error on the stream. An error after reading isbn would mean that the units_sold and revenue members of the old object were unchanged. The effect would be to associate a different isbn with that data.

In this operator, we avoid giving the parameter an invalid state by resetting it to the empty Sales_item if an error occurs. A user who needs to know whether the input succeeded can test the stream. If the user ignores the possibility of an input error, the object is in a usable stateits members are all all defined. Similarly, the object won't generate misleading resultsits data are internally consistent.

When designing an input operator, it is important to decide what to do about error-recovery, if anything.



Indicating Errors

In addition to handling any errors that might occur, an input operator might need to set the condition state (Section 8.2, p. 287) of its input istream parameter. Our input operator is quite simplethe only errors we care about are those that could happen during the reads. If the reads succeed, then our input operator is correct and has no need to do additional checking.

Some input operators do need to do additional checking. For example, our input operator might check that the isbn we read is in an appropriate format. We might have read data successfully, but these data might not be suitable when interpreted as an ISBN. In such cases, the input operator might need to set the condition state to indicate failure, even though technically speaking the actual IO was successful. Usually an input operator needs to set only the failbit. Setting eofbit would imply that the file was exhausted, and setting badbit would indicate that the stream was corrupted. These errors are best left to the IO library itself to indicate.



C++ Primer
C Primer Plus (5th Edition)
ISBN: 0672326965
EAN: 2147483647
Year: 2006
Pages: 223
Authors: Stephen Prata

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net