14.2. Input and Output Operators
FundamentalAs we’ve seen, the IO library uses >>
and <<
for input and output, respectively. The IO library itself defines versions of these operators to read and write the built-in types. Classes that support IO ordinarily define versions of these operators for objects of the class type.
14.2.1. Overloading the Output Operator <<
FundamentalOrdinarily, the first parameter of an output operator is a reference to a nonconst ostream
object. 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 reference to const
of 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 does not change that object.
To be consistent with other output operators, operator<<
normally returns its ostream
parameter.
The Sales_data
Output Operator
As an example, we’ll write the Sales_data
output operator:
ostream &operator<<(ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}
Except for its name, this function is identical to our earlier print
function (§ 7.1.3, p. 261). Printing a Sales_data
entails printing its three data elements and the computed average sales price. Each element is separated by a space. After printing the values, the operator returns a reference to the ostream
it just wrote.
Output Operators Usually Do Minimal Formatting
The output operators for the built-in types do little if any formatting. In particular, they do not print newlines. Users expect class output operators to behave similarly. If the operator does print a newline, then users would be unable to print descriptive text along with the object on the same line. An output operator that does minimal formatting lets users control the details of their output.
TIP
Best Practices
Generally, output operators should print the contents of the object, with minimal formatting. They should not print a newline.
IO Operators Must Be Nonmember Functions
Input and output operators that conform to the conventions of the iostream
library must be ordinary nonmember functions. These operators cannot be members of our own class. If they were, then the left-hand operand would have to be an object of our class type:
Sales_data data;
data << cout; // if operator<< is a member of Sales_data
If these operators are members of any class, they would have to be members of istream
or ostream
. However, those classes are part of the standard library, and we cannot add members to a class in the library.
Thus, if we want to define the IO operators for our types, we must define them as nonmember functions. Of course, IO operators usually need to read or write the nonpublic
data members. As a consequence, IO operators usually must be declared as friends (§ 7.2.1, p. 269).
INFO
Exercises Section 14.2.1
Exercise 14.6: Define an output operator for your Sales_data
class.
Exercise 14.7: Define an output operator for you String
class you wrote for the exercises in § 13.5 (p. 531).
Exercise 14.8: Define an output operator for the class you chose in exercise 7.40 from § 7.5.1 (p. 291).
14.2.2. Overloading the Input Operator >>
FundamentalOrdinarily the first parameter of an input operator is a reference to the stream from which it is to read, and the second parameter is a reference to the (nonconst
) object into which to read. The operator usually returns a reference to its given stream. The second parameter must be nonconst
because the purpose of an input operator is to read data into this object.
The Sales_data
Input Operator
As an example, we’ll write the Sales_data
input operator:
istream &operator>>(istream &is, Sales_data &item)
{
double price; // no need to initialize; we'll read into price before we use it
is >> item.bookNo >> item.units_sold >> price;
if (is) // check that the inputs succeeded
item.revenue = item.units_sold * price;
else
item = Sales_data(); // input failed: give the object the default state
return is;
}
Except for the if
statement, this definition is similar to our earlier read
function (§ 7.1.3, p. 261). The if
checks whether the reads were successful. If an IO error occurs, the operator resets its given object to the empty Sales_data
. That way, the object is guaranteed to be in a consistent state.
INFO
Input operators must deal with the possibility that the input might fail; output operators generally don’t bother.
Errors during Input
The kinds of errors that might happen in an input operator include the following:
- A read operation might fail because the stream contains data of an incorrect type. For example, after reading
bookNo
, 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. - 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 after reading all the data and before using those data:
if (is) // check that the inputs succeeded
item.revenue = item.units_sold * price;
else
item = Sales_data(); // input failed: give the object the default state
If any of the read operations fails, price
will have an undefined value. Therefore, before using price
, we check that the input stream is still valid. If it is, we do the calculation and store the result in revenue
. If there was an error, we do not worry about which input failed. Instead, we reset the entire object to the empty Sales_data
by assigning a new, default-initialized Sales_data
object to item
. After this assignment, item
will have an empty string
for its bookNo
member, and its revenue
and units_sold
members will be zero.
Putting the object into a valid state is especially important if the object might have been partially changed before the error occurred. For example, in this input operator, we might encounter an error after successfully reading a new bookNo
. An error after reading bookNo
would mean that the units_sold
and revenue
members of the old object were unchanged. The effect would be to associate a different bookNo
with those data.
By leaving the object in a valid state, we (somewhat) protect a user that ignores the possibility of an input error. The object will be in a usable state—its members are all defined. Similarly, the object won’t generate misleading results—its data are internally consistent.
TIP
Best Practices
Input operators should decide what, if anything, to do about error recovery.
Indicating Errors
Some input operators need to do additional data verification. For example, our input operator might check that the bookNo
we read is in an appropriate format. In such cases, the input operator might need to set the stream’s condition state to indicate failure (§ 8.1.2, p. 312), even though technically speaking the actual IO was successful. Usually an input operator should 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.
INFO
Exercises Section 14.2.2
Exercise 14.9: Define an input operator for your Sales_data
class.
Exercise 14.10: Describe the behavior of the Sales_data
input operator if given the following input:
(a)0-201-99999-9 10 24.95
(b)10 24.95 0-210-99999-9
Exercise 14.11: What, if anything, is wrong with the following Sales_data
input operator? What would happen if we gave this operator the data in the previous exercise?
istream& operator>>(istream& in, Sales_data& s)
{
double price;
in >> s.bookNo >> s.units_sold >> price;
s.revenue = s.units_sold * price;
return in;
}
Exercise 14.12: Define an input operator for the class you used in exercise 7.40 from § 7.5.1 (p. 291). Be sure the operator handles input errors.