Reading Input with Streams
An iostream has a state that can be examined to tell whether the last operation succeeded.
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.
- 1The function receives an input stream by reference, so it reads from whatever stream the caller supplies.
- 2The vector starts empty and is filled one value at a time.
- 3The 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.
- 4On each pass, the loop asks one question: did the stream accept another integer? If yes, the value is stored and the loop runs again.
- 5When the condition is false, the loop exits and the vector, containing only the successful reads, is returned.
- 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.
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?
- 1Pass 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.
- 2Pass 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.
- 3Pass 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.
- 4After 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.
read_ints returns the vector [1, 2]. The integer 3 is not included, even though it is a valid integer in the input.
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?
Reveal 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.
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.
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 likeWith input 1 2 x 3, expecting read_ints to return [1, 2, 3].
Why it failsThe 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 insteadPredict 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 likeBelieving read_ints stops only when the input runs out.
Why it failsThe end of input is the typical stopping item, but any non-integer stops the loop, because the condition tests readiness.
Do this insteadTreat 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 failsThe loop has no counter and no end marker check. Its only test is whether the last read succeeded.
Do this insteadThink 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 likeCalling clear on the stream after the read of x fails, then reading again without removing x.
Why it failsClearing restores the state but leaves the rejected token in place, so the next read fails on it again.
Do this insteadAfter 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.
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?
- 1List 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.
- 2Find 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.
- 3Identify 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.
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.
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
- 1An 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.
- 2A 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.
- 3An 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.
- 4When 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.