Designing Classes for Input and Output
The iostream library handles input and output for built-in types, standard strings, and programmer-defined types.
Why a Custom Record Needs Printing
Most of the input and output you have met so far moves simple values: whole numbers, decimal numbers, single characters, and text held in standard strings. The library already understands these values, so printing one is routine. Programs quickly outgrow that list, though. A program that tracks people, orders, or measurements will often build its own types to represent each record, and those records are exactly what you will want to print or read.
What do you think happens?
Suppose you have a programmer-defined type Entry that represents one telephone book record, and you write cout << entry with no extra code for Entry. What happens?
Reveal answer
The library has no rule for printing an Entry, so the program does not work until the programmer supplies one.
The source explains that the library cannot know the layout of a user-defined type. A type such as Entry needs its own input and output definition before the stream can handle it.
Three Kinds of Values the Library Handles
It helps to sort the values that flow through iostream into three groups. The first group is the built-in types, such as numbers and characters. The second group is the standard string. The third group is the programmer's own types. The source treats the first two as the starting point and then extends the same idea to the third. The key point is that the library is not fixed to a predefined list of types. A type you write can take part in input and output too.
Entry as a Record Type
The source uses a simple type named Entry as its running example. An Entry represents one entry in a telephone book. A single entry is a bundle of related information about one person, and no built-in type describes that bundle as a whole. That is why a programmer creates a type for it. Once the type exists, it is reasonable to want to print an entry to the screen or read one in from a file. Supporting that desire is exactly what the extension of iostream to programmer-defined types is meant to do.
A user-defined type such as Entry represents one record. It needs its own input and output definition because the library cannot know its layout.
Defining Output for a Custom Type
Picture an Entry moving from your program to the outside world. On the way out, the program must turn the record into characters that a person or a file can display. For built-in types the library already knows how to make that conversion. For an Entry, the programmer has to supply the rules, because the library has no built-in knowledge of what an Entry looks like. Defining input and output for a programmer-defined type therefore means teaching the stream machinery how to handle that new type. The library supplies the machinery, and your code supplies the knowledge.
In C++, that teaching is usually done by writing an overloaded output operator, written as operator<<, for the custom type. The source states the capability plainly but does not show the code, so the following example is an original illustration of the idea rather than the book's own implementation.
Entry: Ada 555-0101- The Entry type groups two related values into one record
- operator<< tells the stream how to print an Entry
- The function returns the stream so that output can keep chaining
Trace the flow in the diagram. The expression cout << e hands the stream and the Entry to the user-defined function. That function reads the name and number fields and writes them to the stream. Control moves from the library to your rule and back again, and the printed characters are the only thing that reaches the output.
How Chained Output Calls Evaluate
A single output statement often contains several items separated by <<. The expression os << a << b does not run as one operation. It is a sequence of calls. Each call writes one item to the stream and returns the stream, and that returned stream becomes the left operand of the next <<. This is why the function in the earlier example ends with return os. Without that return, the next item in the chain would have no stream to write to.
Tracing cout << e for an Entry
Entry e holds name Ada and number 555-0101. What is written when the statement cout << e runs, and in what order?
- 1Call the user-defined operator
cout << e calls operator<< with cout as the stream and e as the record.
- 2Write the name
os <<
e.namewrites Ada and returns os. - 3Write the separator
os << " " writes one space and returns os.
- 4Write the number
os << e.number writes 555-0101 and returns os.
- 5Return the stream
return os gives the stream back to the caller, so the surrounding chain can continue.
Ada 555-0101 is written, in that order, with a single space between the two values.
Defining Input for a Custom Type
Reading an Entry from input runs the process in the opposite direction. Characters coming from an input source must be turned back into the record's parts. The library already performs this conversion for built-in types, so for an Entry the programmer must again supply the rules. In C++ this is typically written as an overloaded input operator, operator>>, which pulls values from the stream and assigns them to the fields of the object. The order of those pulls matters, because each value must be read in the same order the record was written. The source introduces this capability in general terms; the exact input code is not part of the excerpt available here.
Mistakes Beginners Make With Custom Types
Common mistake Assuming the library can print any type automatically
What it looks likecout << entry, with no extra code for Entry
Why it failsThe library cannot know the layout of a user-defined type, so it has no rule for printing one.
Do this insteadDefine input and output for the type, as the source describes, before printing it.
Common mistake Writing an output operator that does not return the stream
What it looks likeoperator<< writes the fields but ends without returning os
Why it failsChained output relies on each call returning the stream as the left operand of the next <<.
Do this insteadEnd the function with return os so the chain can continue.
Common mistake Treating standard strings as needing the same extra code as a custom type, when the library already handles them
Common mistake Expecting this section to explain what happens when a read fails
Why it failsThe source introduces user-defined type I/O but does not explain how errors or end-of-file are handled.
Do this insteadStudy failure and end-of-file handling in the surrounding course material.
Where This Section Stops
The connection is still worth keeping in mind. Reading a custom record is the situation where an incomplete read matters most, because a single Entry usually depends on several pieces of input arriving together. Understanding how a custom type is read is therefore the groundwork for understanding how a read can fail.
Checking Your Understanding of the Chain
A programmer-defined type Book has fields title and year. Sketch an operator<< for Book that prints the title, then a space, then the year, using a chain of output calls. Say which call runs first, and what value each call returns to the next.
Hints
- Start from the left-hand side of the chain: os is the first operand.
- Each call to << returns the stream, which becomes the left operand of the next call.
- The function should end by returning the stream.
Key Takeaways
- The iostream library handles built-in types, standard strings, and programmer-defined types, so it is not limited to a predefined list of types.
- A programmer-defined type such as Entry needs its own input and output definition because the library cannot know its layout.
- Defining output for a custom type means teaching the stream how to turn the record's parts into characters, and chained output works because each call returns the stream.
- Input for a custom type runs in reverse, pulling characters back into the record's parts, and error and end-of-file handling are left for later study.