Defining Custom Input Operations
A custom input operator reads formatted data from an input stream into a specific data structure, and it must check the format and handle errors.
Why Built-In Extraction Is Not Enough
Reading a single number or a single word from a stream is straightforward. The extraction operator already knows how to turn characters into a value of a built-in type, so a program can read the value and move on. Real input is often richer than that. A line of text might describe a whole record, with several fields arranged in a fixed pattern, such as a name and a number placed inside a particular set of punctuation. When your program needs to fill a structure, such as an entry holding a name and a number, from text like that, the built-in operators are not enough. You need to write your own input operator for the type.
The source material uses an entry type holding a name and a number, and it expects the text to look like a brace, a quoted name, a comma, a number, and a closing brace. This article follows that one format. The format itself is not the important part. The important part is the pattern that any custom input operator follows: check each expected piece, stop cleanly when something is wrong, and report the problem back to the caller.
What do you think happens?
Two lines are given to the operator. The first is a well-formed record with the name ann and the number 7. The second is the same record but with the comma between the name and the number missing. Does the entry receive the name ann from the second line?
Reveal answer
No. The entry is left alone and the stream is marked as failed.
The operator checks the comma before it copies anything. When that check fails, the operator marks the stream with a failure flag and never assigns the name or number to the entry. Only when every check passes does the entry get written.
Comparing a Well-Formed Line With a Malformed One
Before reading the mechanics, run the operator on two inputs and compare what happens. The first input is the well-formed line in which an opening brace is followed by the quoted name ann, a comma, the number 7, and a closing brace. The second input is the malformed line that contains the same characters except that the comma is missing between the name and the number. Both lines contain the same characters apart from that one comma, so any difference in the outcome comes entirely from the format check.
Two things are worth watching in each run. The first is what ends up in the entry. The second is what the stream reports once the call returns. The two runs look identical until the operator reaches the comma, and that is the exact spot where its logic decides the outcome.
Tracing the malformed line
The operator reads the line that has the opening brace, the quoted name ann, and the number 7, but no comma between the name and the number. What does the entry contain afterward, and what does the stream report?
- 1Opening brace check
The operator reads one character and confirms it is an opening brace. This check passes, so the operator moves on.
- 2Quote check
The next character is a quote, so this check passes as well.
- 3Name collection
The operator collects characters one at a time until it reaches the closing quote. The collected text ann becomes the name. Nothing has been copied into the entry yet.
- 4Comma check
The operator reads the next character and looks for a comma. The line has a space or a number here instead, so the check fails. This is the decision point.
- 5Failure path
Because the check failed, the operator skips the assignment, registers the failure on the stream, and returns the stream. The number 7 is never read, and the closing brace is never checked.
The entry is not assigned, so it keeps the value it had before the call. The stream is marked with the failure flag, so a test on the stream after the call reports that the read did not succeed.
Checking Each Piece in Order
Inside the operator, each step is a condition that must succeed before the next step runs. The operator reads a character and checks that it is an opening brace. It then reads the next character and checks that it is a quote. Next it collects characters one at a time until it reaches a closing quote, and those characters become the name. After that it reads a character and checks for a comma, then reads a number and a closing brace. Only when all of these checks succeed does the operator copy the name and number into the entry.
The diagram shows the decision structure. Every decision that answers no sends control to the same failure exit, which is why the operator needs only one place that reports errors. The success path is the only path that touches the entry. Because checks happen in order, a failure at an early check means the later pieces of the input are never examined at all.
What Happens on Every Failure
When a check fails, the operator does not throw an error and does not print a message. Instead it marks the stream itself. The source uses a state flag named failbit on the stream for this purpose. Setting that flag records that the read did not succeed, and the caller discovers the result by testing the stream after the call returns.
The reason the entry stays untouched is that the assignment happens only on the success path. The operator collects the name first, but it holds that text without writing it into the entry, and it copies both the name and the number only after every check has passed. A failed read therefore never leaves the caller's object holding partial data, such as a name with no number.
Why the Return Value Works as a Test
An input operation hands back a reference to the stream it read from. Because of that, the whole operation can be used as a condition. Writing a check such as reading into a variable and then asking whether the read worked turns the operation into a question: did the read from the stream into that variable succeed? The answer depends on the state the stream is in after the attempt.
The operator's signature takes the stream and a reference to the entry, and it returns a reference to the stream. Both the input and the output are references, so the operator writes directly into the caller's entry and returns the caller's own stream rather than a copy. Because the stream is returned on both the success path and the failure path, the calling code looks the same either way. The same test works whether the read worked or not, and the failure flag remains visible to any later reads that check the stream first.
In practice this means the caller writes one test around the read. If the test passes, the entry holds a complete record and the program can use it. If the test fails, the program takes its failure branch, and the entry was never modified by the failed attempt.
Anatomy of the Expected Format
Each token in the expected text corresponds to one piece the operator must read and check. The opening brace marks the start of a record. The quoted name is collected up to the closing quote and becomes the name field. The comma separates the name from the number. The number is read and copied into the number field. The closing brace marks the end of the record. A check exists for each of these pieces, and the checks run in the same left-to-right order as the text.
Notice that the format check and the field assignment are separate steps. The operator first confirms that each token is in its expected place. Only when the whole pattern matches does it perform the assignment. This separation is what keeps the target object clean when the input is malformed.
Mistakes When Writing an Input Operator
Common mistake Assigning fields as soon as they are read
What it looks likeCopying the name into the entry right after the closing quote, then discovering that the comma is missing.
Why it failsThe entry now holds partial data even though the read failed. The source describes the target object as assigned only when every check passes.
Do this insteadHold the collected name and number, and copy both into the entry only after the final check succeeds.
Common mistake Printing an error message instead of marking the stream
What it looks likeWriting a message to the screen when the comma check fails and then returning without touching the stream.
Why it failsThe caller has no reliable signal to test after the call, so the failure is easy to miss. The source registers failure through the failbit flag on the stream.
Do this insteadSet the failure flag on the stream and return the stream so the caller can test it.
Common mistake Returning something other than the stream
What it looks likeReturning a copy of the stream, or returning nothing at all, from the operator.
Why it failsThe operation can then no longer serve as a condition, so the uniform success test breaks down.
Do this insteadReturn a reference to the same stream on both the success path and the failure path.
Common mistake Checking the pieces out of order
What it looks likeReading the number before confirming the comma that should come before it.
Why it failsThe format is defined as a fixed sequence, so an out-of-order read can accept text the pattern does not describe.
Common mistake Forgetting that the failure flag persists on the stream after the call
Tracing a Line Yourself
Trace the operator on a line that contains an opening brace, the quoted name bob, a comma, the number 3, and a closing brace, but with the closing brace missing. Work through each check in order. State which check fails first, whether the entry receives the name bob, and what the stream reports after the call returns.
Hints
- Start from the first check and stop at the first decision that answers no.
- Ask whether the entry is assigned before or after the closing brace check.
- Remember that a failed read marks the stream rather than printing a message.
Key Takeaways
- A custom input operator reads formatted data from an input stream into a specific data structure, and it must check the format and handle errors.
- The operator checks each expected piece in order, and the first failed check skips the assignment and registers the failure on the stream.
- Failure is recorded through the failbit flag on the stream, and the target object is assigned only when every check passes, so a failed read never leaves partial data behind.
- The operator returns a reference to its stream, so using the whole operation as a condition tests whether the read succeeded.