AlgoMaster Logo

String Streams (stringstream, istringstream, ostringstream)

Medium Priority15 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

String streams treat an in-memory std::string like a file: write to it with <<, read from it with >>, and use the rest of the C++ stream API. They live in the <sstream> header and come in three flavours: std::ostringstream for building strings, std::istringstream for parsing strings, and std::stringstream for both directions. This chapter covers how each one works, when one fits better than the other, and the small set of pitfalls associated with them.

What a String Stream Is

A regular stream like std::cout is connected to the terminal. An std::ofstream is connected to a file on disk. A string stream is connected to a string buffer that lives in memory. Everything written into it goes into that buffer, and everything read out of it comes from that buffer.

That is the whole idea. Every string stream operation is the stream API already covered, redirected to a string instead of a file or screen.

How to picture it:

Values go into the stream with <<, the stream formats them into characters in its buffer, and .str() returns the finished string. The parsing direction works the same way in reverse: hand the stream a string, then pull formatted values out with >>.

Why use it? Two reasons.

First, building a formatted string out of mixed types (a product name, a price, a stock count, a date) is easy with <<, which does the conversion. There is no need to call std::to_string on each numeric piece and concatenate the results.

Second, parsing a string that contains structured data (a comma-separated row, a whitespace-separated record) is easy with >> and std::getline, without manual character-by-character scanning.

A string stream is a stream with a string instead of a file behind it. Everything else carries over.

ostringstream: Building Strings

std::ostringstream is the output side. Values go in with <<, and they accumulate in an internal string buffer. .str() returns the result as an std::string.

Three different types went in: a string literal, a double, and an int. The stream's << overloads converted each one to characters and appended them to the buffer. .str() returned the concatenated result.

The same thing done with raw std::string concatenation:

Two problems. First, std::to_string(9.99) gives "9.990000" because the default for floating-point conversion has six digits after the decimal. There is no clean way to change that with std::to_string. Second, every + on std::string allocates a new buffer for the intermediate result. Five concatenations means five allocations. The stringstream version writes into a single growing buffer.

Repeated std::string + std::string is O(n^2) in the total length when chained, because each step builds a new string. std::ostringstream grows its buffer geometrically, so building a long string is O(n) total. For short strings the difference does not matter; for long ones it does.

ostringstream also handles types that std::to_string does not, like user-defined types that overload operator<<. If a Product class has a stream insertion operator, out << product works without conversion code.

istringstream: Parsing Strings

std::istringstream is the input side. Construct it from a string, then >> values out of it. Reading from it works the same as reading from std::cin or an std::ifstream, except the data source is the string passed in.

>> skips whitespace by default and reads one token at a time, converting it to the type of the destination variable. The first >> reads up to the next whitespace and copies the characters into name. The second one parses a double from the next token. The third parses an int. Same behaviour as std::cin.

A common use is parsing many lines of structured data. Read each line from a file (using std::ifstream and std::getline), feed it into an istringstream, then pull the fields out.

Each line becomes its own fresh istringstream. Inside the loop, the three >> operations pull the three fields out of that line. Once the loop body ends, the istringstream goes out of scope and gets destroyed. The next iteration builds a new one. No state leaks between lines.

The product names in the example use hyphens because >> treats whitespace as a separator. A name with a space in it ("USB Cable") would be split across two reads. That is a real limitation, and the standard fix is to switch to a different delimiter, covered with getline shortly.

stringstream: Bidirectional

std::stringstream does both directions. The same object accepts << and >>. The internal buffer is shared between reads and writes.

This is useful when building a string and immediately consuming it, or when a single function both produces and inspects intermediate text. In practice, most code uses the one-directional types because the intent is clearer. If a function only builds a string, std::ostringstream says exactly that, and the compiler will not allow accidental reads. If a function only parses, std::istringstream does the same in reverse.

Use std::stringstream when both directions are genuinely needed: a parser that builds tokens incrementally, a formatter that pre-processes input, a test helper that injects fake input and inspects the response. For straight build-or-parse work, prefer the dedicated type.

.str() to Get and Set Contents

.str() has two roles. With no argument, it returns the current contents of the stream's buffer as an std::string. With a string argument, it replaces the buffer's contents.

Calling .str() does not drain the stream. It returns a copy of what is there now. The stream keeps its buffer and its write position, so the next << continues from where it left off.

The single-argument form replaces the buffer:

in.str("14.99 7") swaps the underlying string for a new one. The stream is now positioned at the start of the new buffer (most implementations reset the read position). There is a catch on the next line: in.clear(). The previous parse hit end-of-file, which set the eof bit on the stream. Even though the buffer was replaced, the error state from the previous read is still there. Without clear(), the next >> does nothing because the stream still considers itself in a failed state.

.str() (the no-argument getter) returns a copy of the buffer's contents. For long strings, that is an O(n) allocation. If only the result is needed and the stream is about to be destroyed anyway, that copy is unavoidable; in C++20 use .view() (returns a std::string_view) on stringstream to avoid it. For pre-C++20 code, accept the copy.

Resetting State with .clear()

Every C++ stream has an error state separate from its contents. The state is a small set of flags: goodbit (everything fine), eofbit (reached the end), failbit (the last operation failed), badbit (something is corrupt). After a parse hits the end of the buffer, eofbit is set. After a parse fails to convert (like reading "abc" as an int), failbit is set. Once either flag is set, every subsequent operation on that stream does nothing.

.clear() resets those flags back to goodbit. It does not touch the buffer.

The name clear is misleading. It does not clear the contents; it clears the state. To clear the contents, call .str(""). To clear the state, call .clear(). They are independent operations, and both are usually needed to fully reuse a stream object.

A common version of the bug: parse one string, get the expected result, replace the buffer for the next parse, see that the new parse does nothing, spend twenty minutes debugging because the variables look uninitialized. The fix is one line: in.clear() after in.str(newContents).

Parsing CSV Lines with getline and a Delimiter

std::getline has a three-argument form that accepts any character as the delimiter. The two-argument version reads up to '\n'. The three-argument version reads up to the given character.

That makes parsing a comma-separated row easy. Wrap the line in an istringstream, then getline repeatedly with ',' as the delimiter.

The first field is "USB Cable" with the space preserved, which is what >> could not do. Each getline call reads up to (but not including) the next comma, copies the characters into field, and consumes the comma. The loop terminates when the stream runs out.

The fields come back as strings. To get numeric values, convert them with std::stod or std::stoi:

Each getline pulls one comma-separated field. The numeric fields go through std::stod and std::stoi after extraction. That is the standard pattern for CSV-ish data with mixed types.

This approach does not handle quoted fields with commas inside them (real-world CSV has plenty of "Smith, John" cases). Production CSV parsers handle escaping rules that go beyond what a one-character delimiter can do. For internal data formats under user control, the simple getline approach is fine. For real CSV files from spreadsheets, use a dedicated CSV library.

Building a Sales Report Line

ostringstream is useful for building a single formatted string out of mixed values and passing it somewhere as a std::string: a log line, a return value, a row to append to a file, an entry in a vector. A concrete example: format a one-line summary for each order in a daily sales report.

formatOrderLine builds a string from five mixed values (two ints, two doubles, one std::string) and returns it. The function is self-contained: no global state, no shared buffer to clear, a fresh ostringstream that gets thrown away when the function returns. The + concatenation alternative would need explicit std::to_string calls for every numeric value, which makes the code longer and less readable. The stringstream version reads top to bottom and looks like the format itself.

The output here uses C++'s default formatting for double (no trailing zeros, no fixed decimal width).

When to Skip stringstream

For single-value conversions, std::to_string, std::stoi, std::stod are simpler than spinning up a stringstream. They take one value and return one converted result. No buffer, no state, no clear() calls.

The catch with std::to_string is the default formatting: floating-point values get six digits after the decimal, with no way to change that. For an interactive log line where "9.990000" looks ugly, ostringstream is the cleaner choice. For an internal id or a serialised integer, std::to_string is fine.

A side-by-side comparison:

ScenarioBest toolWhy
Convert one int or long to stringstd::to_stringOne line, no state
Convert one double and care about formattingstd::ostringstreamManipulators control digits; to_string always gives 6
Concatenate two or three short stringsoperator+Direct, no header
Build a long formatted line from mixed typesstd::ostringstreamOne buffer, no per-piece allocations
Parse "42" into an intstd::stoiOne call, throws on failure
Parse "USB-Cable 9.99 25" into name + price + stockstd::istringstream with >>One pass, one error state to check
Parse a comma-separated rowstd::istringstream + std::getline(in, field, ',')Handles fields with spaces
Round-trip text through a parser and backstd::stringstreamBidirectional in one object
ApproachBuild "Order #1001 - Alice - $49.99"AllocationsReadability
std::string + with std::to_stringLong, ugly default for doubleMany (one per +)Low
std::ostringstreamReads like the format itselfOne growing bufferHigh
Plain printf to a char bufferRequires manual buffer sizeOneMedium (C-style)
C++20 std::formatCleanest of allOneHighest

The last row is worth mentioning. C++20 added std::format, which allows std::format("Order #{} - {} - ${:.2f}", id, name, total) and is the cleanest tool for the job when the compiler supports it. For pre-C++20 code or compilers without full C++20 support, ostringstream is the standard answer.

A pitfall that traps every newcomer: forgetting .clear() before re-reading from a stream after EOF or after a failed parse. The state bits stay set, and the next >> does nothing. When a variable does not change after a parse, check .fail() or .eof() and call .clear().

Quiz

String Streams Quiz

10 quizzes