Concepts / Using String Streams

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.

  • Programming

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?

  • It returns a number

  • It throws a runtime_error

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

Empty streamCharacters writtensource value as textTarget value readcharacters parsed as TargetOnly trailingwhitespace leftwritereadleftover check
How the same stringstream changes state as a value is written in and then read back out.

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.

Argument argWrite arg into streamRead Target fromstreamAnything besidestrailing whitespace?Return resultThrow runtime_errorsucceedsfailssucceedsfailsyesno
Which check runs next at each step, and which check sends the flow to a returned value or to a thrown runtime_error.

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.

Key point

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.

Value of originaltypethe Source argumentString representationcharacters held in thestreamValue of target typethe Target resultwritten into streamread out as Target
The order in which a value travels through the conversion, and where each conversion step happens.

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.

Worked example

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.

  1. 1
    Write 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.

  2. 2
    Read 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.

  3. 3
    Leftover 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.

  4. 4
    Result

    Because the checks are joined with or, the failed leftover check causes the function to throw runtime_error with the message to<>() failed. The departure from expectation is at the leftover check, not at the read, even though the read produced 12.

Result

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

Definition

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.

Type AString form of AConversion can bedefinedType BString form of BRule does not applyType Cno string formhashasboth requiredboth requiredlacks string form
A conversion can be defined only when both types have a string representation. A type without one falls outside the rule.

Mistakes Beginners Make with Conversions

  • Common mistake

    Assuming a failed conversion tells you which step failed

    Why it fails

    The source states that the function throws the same runtime_error message whenever any check fails. The message does not identify the write, read, or leftover check.

    Do this instead

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

    The text "12 apples" yields 12 on the read, but the leftover check still fails because " apples" remains.

    Why it fails

    The checks are joined with or, so the conversion succeeds only if every check passes, not only the read.

    Do this instead

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

    The source says a conversion is defined between two types only when both have a string representation. Defining it is a separate step.

    Do this instead

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

    The rule names both types, so a string form on one side alone does not satisfy it.

    Do this instead

    Check each of the two types separately before concluding that the rule applies.

Practice Tracing a Conversion

EASY

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.
MEDIUM

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.

Related concepts

More concepts starting with U