Concepts / Reading Input with Streams

Reading Input with Streams

An iostream has a state that can be examined to tell whether the last operation succeeded.

  • Programming

A Loop That Reads Until Something Goes Wrong

Reading a sequence of values from input is one of the most common jobs a program does, and an input stream gives you a convenient way to do it. An iostream carries a state that you can examine to determine whether an operation succeeded. Most of the time you use that state without thinking about it, through a single condition in a loop. This article looks closely at that condition, because it is simple to write and easy to misread.

The question this article answers is: when a read loop finally stops, why did it stop, and what has the container received up to that point? To answer it, you need to know exactly what the while condition tests. Before reading the explanation, try a prediction, because the answer is more interesting than it first looks.

What the Read Loop Tests

The function below reads integers from an input stream into a vector and returns the vector. Notice that the while statement has a single condition, and that the loop does not count values or look for an end marker.

cpp
Walk through it
  1. 1
    The function receives an input stream by reference, so it reads from whatever stream the caller supplies.
  2. 2
    The vector starts empty and is filled one value at a time.
  3. 3
    The expression is >> i attempts to read one integer into i. The operation returns a reference to the stream, and testing that stream yields true only if it is still ready for another operation.
  4. 4
    On each pass, the loop asks one question: did the stream accept another integer? If yes, the value is stored and the loop runs again.
  5. 5
    When the condition is false, the loop exits and the vector, containing only the successful reads, is returned.
Notes on the code
  • The vector collects every successfully read integer
  • The only loop condition is the stream operation itself

The key idea is that the condition is a readiness test. It does not ask whether the loop has reached a particular count or a particular marker. It asks only whether the most recent read produced a usable integer. The loop therefore stops at the first item that is not an integer. That item will typically be the end of input, but the mechanism is the same for any non-integer, so a bad token in the middle of the data stops the loop just as surely.

The stream's state holds the information the readiness test reports on. The I/O state of a stream includes formatting information, the error state (for example, whether end of input has been reached), and the kind of buffering in use. For error handling, the error state is the part that matters, because it is what the test checks.

Tracing the Loop on 1 2 x 3

Suppose the input stream contains the text 1 2 x 3. The first two items are integers, the third is not, and a fourth integer follows the bad item. Trace what read_ints does, one pass at a time, and watch both the stream's readiness and the vector's contents.

Worked example

Tracing read_ints on the input 1 2 x 3

What does read_ints return when the stream contains 1 2 x 3, and why does the fourth value never reach the vector?

  1. 1
    Pass 1

    The read of is >> i succeeds and i holds 1. The stream is still ready, so 1 is pushed onto the vector and the loop continues.

  2. 2
    Pass 2

    The read succeeds again and i holds 2. The stream is still ready, so 2 is pushed and the loop continues. The vector now holds 1 and 2.

  3. 3
    Pass 3

    The next item is x, which cannot be stored as an int. The read fails, the stream is no longer ready, and the condition is false. The loop exits without pushing anything.

  4. 4
    After the loop

    The function returns the vector, which holds only the successful reads. The 3 that follows the bad item is never read, because the loop has already ended.

Result

read_ints returns the vector [1, 2]. The integer 3 is not included, even though it is a valid integer in the input.

Pass 1: read 1state: ready, vector [1]Pass 2: read 2state: ready, vector [1, 2]Pass 3: read xstate: not ready, vector[1, 2]Loop exitsreturns [1, 2]condition truecondition truecondition false
In each pass, the value read, the stream state afterward, and the vector contents show where the loop stops and why.

Predicting Before Revealing

What do you think happens?

The stream contains 1 2 x 3. What does read_ints return before you read the explanation?

  • [1, 2, 3]

  • [1, 2]

  • [1]

  • An empty vector

Reveal answer
Answer:

[1, 2]

The loop accepts 1 and 2, then the read of x fails and the stream is no longer ready. The condition becomes false and the loop exits, so the 3 after the bad token is never read.

How the Stream State Changes Between Reads

The same stream can be in different conditions at different points in a program. It starts ready. It can reach the end of input. It can hit an error. And an error that was not serious can be cleared, which makes the stream ready again. The diagram below shows these conditions as states, with the events that move the stream between them.

readyvalue storedend of inputerror recordedserious errorerror clearedread succeedsloop repeatsinput exhaustedread failsseriousnot seriousreading continues
Each attempted read either stores a value and keeps the stream ready, or leaves the stream unable to continue. A non-serious error can be cleared and the stream returns to ready.

Notice that end of input is a terminal condition. Reaching it is the usual reason a read loop finishes. An error, by contrast, leads to a decision. The program must decide whether the problem is serious. A serious error stops reading. A non-serious one can be cleared so that reading continues.

Recording and Clearing a Non-Serious Error

Reading a stream is not always a simple choice between success and stopping. A user can set the state to reflect that an error has occurred, and can clear the state if the error was not serious. This gives the program a choice. A failed read can end the whole operation, or it can be recorded, handled, and then followed by further reading.

state: failstate: goodunread input: x 3next item to readunread input: x 3same positionclear the errornot consumed
Clearing the recorded error changes the state from fail back to good. The unread input remains at the same position, ready for the next read.

The before and after view makes one point clear. Clearing the error changes the flag that says the last operation failed. It does not move the input. The rejected token is still waiting at the same position, so the next read attempt starts from there.

Where the Rejected Token Stays

After a read fails on a token of the wrong type, the unread input does not vanish. In the input 1 2 x 3, the failed read was aimed at the x. Because the read could not turn x into an integer, the stream is left at that point in the input, and the x is still there to be examined. A program that clears the error and immediately tries to read an integer again will meet the same x unless it does something to move past it.

Mistakes Beginners Make with Read Loops

  • Common mistake

    Assuming the loop reads every value in the input

    What it looks like

    With input 1 2 x 3, expecting read_ints to return [1, 2, 3].

    Why it fails

    The loop stops at the first value that is not the expected type. The 3 comes after the bad token, so it is never read.

    Do this instead

    Predict the result by finding the first item that is not an integer. Everything before it is kept, and everything after it is not read.

  • Common mistake

    Thinking the loop stops only at the end of input

    What it looks like

    Believing read_ints stops only when the input runs out.

    Why it fails

    The end of input is the typical stopping item, but any non-integer stops the loop, because the condition tests readiness.

    Do this instead

    Treat any unreadable token as a stopping point for the loop, not just the end marker.

  • Common mistake

    Expecting the loop to count values

    Why it fails

    The loop has no counter and no end marker check. Its only test is whether the last read succeeded.

    Do this instead

    Think of the condition as asking, on each pass, whether the stream accepted another integer.

  • Common mistake

    Clearing the error and expecting the loop to continue past the bad token

    What it looks like

    Calling clear on the stream after the read of x fails, then reading again without removing x.

    Why it fails

    Clearing restores the state but leaves the rejected token in place, so the next read fails on it again.

    Do this instead

    After clearing, consume or discard the bad token before reading again.

Finding the Iteration Where the Result Diverged

When a read loop returns something you did not expect, the debugging task is to find the iteration where the stream stopped being ready. Do not start by asking what the loop was supposed to produce. Ask what the input held at each pass. The vector is only a record of the successful reads, so the first failed read is where the result diverged from your expectation.

Worked example

Locating the divergence

A program expected read_ints to return [4, 5, 6, 7] from the input 4 5 6 seven 8, but it returned [4, 5, 6]. Where did the result diverge?

  1. 1
    List the tokens

    The input tokens in order are 4, 5, 6, seven, 8. The expected result assumed seven would be read as 7, but it is written as a word.

  2. 2
    Find the first failed read

    Passes one to three read 4, 5, and 6 successfully. Pass four attempts to read seven, which is not an integer, so the stream becomes not ready.

  3. 3
    Identify the divergence

    The first value that differs from expectation is the missing 7. The divergence is at the fourth pass, where the failed read on seven occurred.

Result

The result diverged at the fourth iteration, where the input token seven could not be read as an integer. The fix belongs in the input data or in how the bad token is handled, not in the loop's counting logic, because the loop has none.

MEDIUM

Given the input 10 20 abc 30 40, predict the vector returned by read_ints. Then name the exact pass where the stream stopped being ready, and state what the input held at that pass.

Hints
  • Count only the integers before the first token that is not an integer.
  • The pass where the read fails is the one that attempts to read abc.

Key Points to Carry Forward

  1. 1
    An iostream's state can be examined to tell whether the last operation succeeded, and the condition in a read loop tests that readiness on every pass.
  2. 2
    A loop such as while (is >> i) stops at the first value that is not the expected type, typically the end of input, and values after that point are not read.
  3. 3
    An error can be recorded in the stream's state. If the error is not serious, the state can be cleared so the stream is ready again, but clearing does not skip the bad input.
  4. 4
    When a result is unexpected, find the first iteration where the stream stopped being ready and ask what the input held at that point.

Key Takeaways

  • An iostream's state can be examined to tell whether the last operation succeeded, and the while condition in a read loop tests that readiness on every pass.
  • The loop stops at the first value that is not the expected type, typically end of input, and values after that point are not read.
  • A non-serious error can be recorded in the stream's state and cleared, but clearing does not move past the rejected input.
  • To debug an unexpected result, find the first failed read and ask what the input held at that iteration.

Related concepts

More concepts starting with R