Concepts / Designing Classes for Input and Output

Designing Classes for Input and Output

The iostream library handles input and output for built-in types, standard strings, and programmer-defined types.

  • Programming

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?

  • It prints the members of Entry automatically

  • It prints only the first member

  • The library has no rule for Entry, so the program does not work until the programmer supplies one

Reveal answer
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.

iostream libraryBuilt-in typesnumbers, charactersLibrary already knowsStandard stringtextLibrary already knowsProgrammer-definedtypese.g. EntryProgrammer suppliesruleshandleshandlesextended to
How built-in types, standard strings, and programmer-defined types all connect to the same iostream library, and which of them need extra code from the programmer.

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.

Definition

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.

cpp
Output
Entry: Ada 555-0101
Notes on the code
  • 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
coutoperator<<user-defined rulename fieldAdaOutput streamcharacters writtenEntry objectnumber field555-0101passes streampasses recordreadsreadswrittenwritten
When cout << entry runs, the record's fields move into the output stream through the user-defined function, which is where control passes from the library to the programmer's rule.

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.

os << e.namefirst callos << " "returns osos << e.numberreturns osreturn osstream handed backstreamstreamstream
The steps that run inside operator<< for an Entry, in order. Each step returns the same stream, which is passed to the next step.
Worked example

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?

  1. 1
    Call the user-defined operator

    cout << e calls operator<< with cout as the stream and e as the record.

  2. 2
    Write the name

    os << e.name writes Ada and returns os.

  3. 3
    Write the separator

    os << " " writes one space and returns os.

  4. 4
    Write the number

    os << e.number writes 555-0101 and returns os.

  5. 5
    Return the stream

    return os gives the stream back to the caller, so the surrounding chain can continue.

Result

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 like

    cout << entry, with no extra code for Entry

    Why it fails

    The library cannot know the layout of a user-defined type, so it has no rule for printing one.

    Do this instead

    Define 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 like

    operator<< writes the fields but ends without returning os

    Why it fails

    Chained output relies on each call returning the stream as the left operand of the next <<.

    Do this instead

    End 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 fails

    The source introduces user-defined type I/O but does not explain how errors or end-of-file are handled.

    Do this instead

    Study 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

MEDIUM

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.

Related concepts

More concepts starting with D