14.2. Input and Output OperatorsClasses 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 <<
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 OperatorWe 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 FormattingClass designers face one significant decision about output: whether and how much formatting to perform.
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 FunctionsWhen 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.
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.
The Sales_item Input OperatorThe 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 InputOur Sales_item input operator reads the expected values and checks whether an error occurred. The kinds of errors that might happen include:
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 ErrorsIf 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.
Indicating ErrorsIn 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. |