Using String Streams
A stringstream can be used for both reading and writing, which lets one generic operation convert any type with a string representation into another.
Why Programs Need Type Conversion
Many values reach a program as text. A number typed by a user, a field read from a file, or a word taken from a command line all start out as characters. Before a program can do arithmetic with such a value, or compare it with a number, those characters have to be turned into the right type. This article approaches that job from one concrete question: if two types both have a string representation, can one general operation convert a value of the first type into the second?
The conversion is worth studying closely because it is not a single action. It is a short sequence of checks, and the result depends on which check passes or fails. Before reading on, make a prediction about a conversion and keep your reasoning in mind, because that reasoning is what the rest of the article makes explicit.
What do you think happens?
You ask the generic conversion to produce an int from the text "abc". Will it return a number, or will it throw a runtime_error?
Reveal answer
It throws a runtime_error
The text "abc" cannot be read as an int, so the read check fails. The function throws whenever any of its checks fails, and the read is one of them. The write check and the leftover check never make the conversion succeed on their own, so the failed read is enough to produce the throw.
One Stream, Two Jobs
A stringstream is a stream that holds characters in memory. The source states that it can be used for both reading and writing. Those two uses are normally separate jobs: one puts data in, and the other takes data out. Type conversion combines them. Data goes into the stream in its original form, and then the same stream is asked to produce a value of a different type.
The same object is therefore first a destination and then a source. Writing fills it with characters. Reading consumes those characters to build a value of the target type. Nothing about the stream needs to know what the characters mean. It only needs the source value to be written as characters and the target value to be read from them.
This is why the approach works for any pair of types that each have a string representation. The source describes a single operation that converts any type with a string representation into another type that also has one. The operation is written as a function template with two type parameters, both defaulting to string. The first parameter, named Target, is the type you want back. The second, named Source, is the type of the argument you pass in. The function is called to, takes one argument named arg, and returns a value of type Target. Because both parameters default to string, a call that names no types falls back to string for both. Naming int as the target selects the type you need. The examples in this article assume this function is available under the name to.
Three Checks Run in Order
Writing a value into the stream and reading a value out of it do not guarantee success. The source's condition chains three tests with logical or. The tests run one after another, and the first one that fails causes the function to throw. The first test asks whether writing the argument into the stream worked. The second asks whether reading the result out of the stream worked. The third asks whether anything other than trailing whitespace remains in the stream after the read.
Because the tests are joined with or, a failure in one test means the remaining tests are not evaluated. If the write fails, the read is never attempted. If the read fails, the leftover check never runs. The returned result is the only exit for success, and the throw is the only exit for failure. Read the diagram from top to bottom and you will see that a conversion succeeds only when it passes all three checks in sequence.
A conversion can fail at the write, at the read, or at the leftover check. Each check corresponds to a different kind of failure, so knowing which check fired tells you which part of the conversion went wrong.
Tracing a Value Through the Conversion
Consider the route a value takes when it is converted. A value of the original type is written out as a string representation, and that text is then used to produce a value of the target type. The string form sits in the middle. The value starts in its own type, becomes characters in the stream, and ends as a value of the new type.
The source is careful about what this example shows. It does not claim that the conversion is already written for you or that it runs without effort. It shows that the condition for defining the conversion is met. The stream mechanism described above is the part that carries out the definition, and the string representation is what allows the value to pass from one type to the other.
Locating Where a Conversion Fails
When a conversion throws, the error alone does not say which step failed. The source says the function throws a runtime_error with the message to<>() failed whenever any check fails, so the message is the same for all three failure points. To locate the failure, you must trace the checks yourself and find the first one that did not hold. The worked example below does this for a case where the result differs from what a programmer might expect.
A text that looks like a number
A program reads the text "12 apples" and calls the generic conversion to produce an int. The programmer expects 12. Trace the three checks and identify where the result departs from that expectation.
- 1Write check
The argument is written into the stream as characters. The write itself succeeds, so the flow moves on to the read. Nothing has yet been converted to an int.
- 2Read check
The read attempts to produce an int from the leading characters. The digits 12 can be read as an int, so the read succeeds and the value 12 is taken out. The read has produced the number the programmer expected.
- 3Leftover check
The third test asks whether anything other than trailing whitespace remains. The characters " apples" are still in the stream. They are substantive, not whitespace, so the leftover check fails.
- 4Result
Because the checks are joined with or, the failed leftover check causes the function to throw
runtime_errorwith the message to<>() failed. The departure from expectation is at the leftover check, not at the read, even though the read produced 12.
The conversion throws. The programmer expected the value 12, but the leftover check rejects the input because text remains after the number. The failure is located at the third check, which is the step where the result departs from the expected value.
Notice that the read step looked successful when viewed alone. A programmer who only checked whether a number came out would be misled. Tracing all three checks in order is what reveals the real stopping point.
When the Rule Applies
A string representation is a textual form of a value. The source's rule depends on this idea: a conversion can be defined between two types when both of them have a string representation. The string form acts as a common middle ground. A value can be written out as text, and text can be read back into a value, so the string form gives a route from one type to another.
The rule has two parts. The first is scope: the conversion concerns two specific types. The second is the requirement: both types must have a string representation before the definition is possible. The condition applies to both types. Having a string form on only one side does not satisfy the rule, because the rule names both types. Also note that the rule permits a conversion to be defined; defining it is a separate step.
Mistakes Beginners Make with Conversions
Common mistake Assuming a failed conversion tells you which step failed
Why it failsThe source states that the function throws the same
runtime_errormessage whenever any check fails. The message does not identify the write, read, or leftover check.Do this insteadTrace the three checks in order yourself to find the first one that did not hold.
Common mistake Treating a successful read as proof that the whole conversion succeeded
What it looks likeThe text "12 apples" yields 12 on the read, but the leftover check still fails because " apples" remains.
Why it failsThe checks are joined with or, so the conversion succeeds only if every check passes, not only the read.
Do this insteadCheck that the read produced the value and that nothing substantive remains after it.
Common mistake Expecting a conversion to exist for any pair of types automatically
Why it failsThe source says a conversion is defined between two types only when both have a string representation. Defining it is a separate step.
Do this insteadConfirm that both types have a string representation before attempting to define the conversion.
Common mistake Requiring a string form on only one of the two types
Why it failsThe rule names both types, so a string form on one side alone does not satisfy it.
Do this insteadCheck each of the two types separately before concluding that the rule applies.
Practice Tracing a Conversion
Using the generic conversion to, a program calls to<int>("7") on the text "7". Write down the outcome of each of the three checks in order. Then say whether the function returns a value or throws, and name the check that decides the outcome.
Hints
- Start with the write check. Writing the text "7" into the stream succeeds.
- The read check asks whether an int can be produced from the characters. Consider whether anything is left in the stream after that read.
- Remember that the checks are joined with or, so the first failure stops the function.
Take the text "3 4" and call to<int> on it. Predict which check fails, and state what the stream holds at that moment. Then explain why the failure at that check means the result differs from the number 3 a programmer might expect.
Hints
- Ask what remains in the stream after the read produces 3.
- Recall that only trailing whitespace is allowed after the read.
Key Takeaways
- A stringstream can be used for both writing and reading, so one stream can carry a value in its original form and then produce a value of another type.
- The generic conversion writes the argument, reads the target, and checks that nothing substantive remains. The three checks are joined with or, so the first failure stops the function and throws
runtime_error. - A failing conversion can fail at the write, the read, or the leftover check. Because the error message does not name the step, you locate the failure by tracing the checks in order.
- A conversion between two types can be defined through string representation only when both types have a string form. Defining the conversion is a separate step after that condition is met.