Skip to content
C++
Library
since C++98
Basic

sstream

Header providing std::istringstream, std::ostringstream, and std::stringstream for in-memory formatted I/O on std::string buffers.

<sstream>since C++98

The <sstream> header provides stream classes that perform formatted I/O against an in-memory std::string buffer rather than a file or terminal.

Overview

<sstream> exposes three concrete class templates and their wchar_t counterparts:

Class templateDirectionBase
std::basic_istringstream<CharT>read-onlystd::basic_istream<CharT>
std::basic_ostringstream<CharT>write-onlystd::basic_ostream<CharT>
std::basic_stringstream<CharT>read-writestd::basic_iostream<CharT>

The char specialisations β€” std::istringstream, std::ostringstream, std::stringstream β€” cover the vast majority of use cases. Wide variants (std::wistringstream, etc.) follow the same interface with wchar_t as the character type.

The underlying buffer is a std::basic_stringbuf<CharT>. Every stream operation that works on std::cout or std::cin works identically here: format flags, manipulators, operator<<, operator>>, getline, and locale-sensitive conversions all apply.

str() member exists in two forms:

  • str() const β€” returns the current buffer contents as a std::string.
  • str(const std::string&) β€” replaces the buffer and resets the stream position.

Since C++20, additional overloads accept rvalue references and std::string_view.

When to prefer alternatives: For straightforward value formatting, std::format (C++20, <format>) is faster, type-safe, and has no mutable state. For high-performance scalar ↔ string conversion in tight loops, std::to_chars / std::from_chars (C++17, <charconv>) avoids locale overhead and heap allocation entirely.

Syntax

cpp
#include <sstream>

// Write values into a string
std::ostringstream oss;
oss << value;
std::string result = oss.str();

// Parse values from a string
std::istringstream iss(source);
iss >> value;

// Round-trip: write then read back from the same buffer
std::stringstream ss;
ss << value;
ss.seekg(0);   // rewind get pointer before reading
ss >> value;

Examples

Formatted string construction

cpp
#include <sstream>
#include <iomanip>
#include <string>

std::string format_hex_color(unsigned r, unsigned g, unsigned b) {
    std::ostringstream oss;
    oss << '#'
        << std::hex << std::uppercase << std::setfill('0')
        << std::setw(2) << r
        << std::setw(2) << g
        << std::setw(2) << b;
    return oss.str();
}
// format_hex_color(255, 128, 0) β†’ "#FF8000"

Since C++20 this collapses to std::format("#{:02X}{:02X}{:02X}", r, g, b).

Parsing whitespace-delimited tokens

cpp
#include <sstream>
#include <vector>
#include <string>

std::vector<int> parse_ints(const std::string& line) {
    std::istringstream iss(line);
    std::vector<int> out;
    int x;
    while (iss >> x) {
        out.push_back(x);
    }
    return out;
}
// parse_ints("10 -3 42 7") β†’ {10, -3, 42, 7}

operator>> skips leading whitespace by default and sets failbit on a type mismatch, so the loop terminates cleanly on a non-integer token or end-of-string.

Parsing structured lines

The canonical pattern for delimiter-separated fields is std::getline into an istringstream:

cpp
#include <fstream>
#include <sstream>
#include <string>

void process_tsv(std::istream& file) {
    std::string line;
    while (std::getline(file, line)) {
        std::istringstream row(line);
        std::string timestamp, level, message;
        std::getline(row, timestamp, '\t');
        std::getline(row, level,     '\t');
        std::getline(row, message);
        // ...
    }
}

Custom type round-trip

std::stringstream is the standard mechanism for verifying that operator<< and operator>> form a symmetric pair:

cpp
#include <sstream>
#include <cassert>

struct Vec2 { double x, y; };

std::ostream& operator<<(std::ostream& out, const Vec2& v) {
    return out << v.x << ' ' << v.y;
}

std::istream& operator>>(std::istream& in, Vec2& v) {
    return in >> v.x >> v.y;
}

void roundtrip_test() {
    const Vec2 original{3.14, -2.71};
    std::stringstream ss;
    ss << original;

    Vec2 recovered{};
    ss >> recovered;

    assert(recovered.x == original.x && recovered.y == original.y);
}

Note: no seekg(0) is required here. A default-constructed stringstream starts with the get pointer at position 0, so extraction immediately follows insertion without a seek.

Reusing a stream object in a loop

Constructing an ostringstream inside a loop allocates a new stringbuf each iteration. Hoist the object and reset it instead:

cpp
#include <sstream>
#include <vector>
#include <string>

std::vector<std::string> to_strings(const std::vector<double>& values) {
    std::ostringstream oss;
    std::vector<std::string> out;
    out.reserve(values.size());

    for (double v : values) {
        oss.str("");   // clear buffer content
        oss.clear();   // clear eofbit/failbit β€” both calls are required
        oss << v;
        out.push_back(oss.str());
    }
    return out;
}

Locale-sensitive parsing

String streams inherit the global locale by default. Override it with imbue() for controlled locale behaviour:

cpp
#include <sstream>
#include <locale>

double parse_german(const std::string& s) {
    std::istringstream iss(s);
    iss.imbue(std::locale("de_DE.utf8"));
    double value{};
    iss >> value;
    return value;
}
// parse_german("1.234,56") β†’ 1234.56

Chrono parsing (C++20)

std::chrono::parse reads from any std::istream, making istringstream the natural adapter for timestamp strings:

cpp
#include <sstream>
#include <chrono>

std::chrono::sys_seconds parse_timestamp(const std::string& s) {
    std::istringstream iss(s);
    std::chrono::sys_seconds tp;
    iss >> std::chrono::parse("%Y-%m-%d %H:%M:%S", tp);  // C++20
    if (iss.fail()) throw std::runtime_error("bad timestamp: " + s);
    return tp;
}

Best Practices

Use <format> for pure output (C++20). std::format is stateless, does not allocate a stringbuf, and compiles format strings at compile time. Prefer it for any new code where you control the format string.

Use <charconv> for scalar conversion in hot paths (C++17). std::to_chars and std::from_chars are locale-independent, exception-free, and operate directly on a char buffer. They are routinely 5–10Γ— faster than the stream equivalent for numeric conversions.

Pin the locale when output must be portable. Calling oss.imbue(std::locale::classic()) on construction prevents the global locale from injecting commas or non-ASCII decimal separators into numeric output.

Call str() once and store the result. Each invocation copies the internal buffer. Store oss.str() in a local variable rather than calling it multiple times.

Common Pitfalls

Forgetting clear() when reusing. str("") empties the buffer but does not touch the stream state flags. If the stream reached EOF or a parse error, eofbit or failbit remain set and all subsequent extractions silently fail:

cpp
std::istringstream iss("42");
int x;
iss >> x;         // x == 42, eofbit set
iss.str("99");    // buffer replaced, eofbit still set
iss >> x;         // FAILS silently β€” x unchanged
iss.clear();      // now clear error state
iss >> x;         // x == 99

Reading from a written stringstream without seeking. After writing to a stringstream, the get pointer sits at the write position. Extraction from that point reads nothing:

cpp
std::stringstream ss;
ss << "hello";
std::string s;
ss >> s;          // s is empty β€” get pointer is at end
ss.seekg(0);      // rewind
ss >> s;          // s == "hello"

Alternatively, construct a fresh std::istringstream(ss.str()) for a clean read.

Constructing istringstream from a string and then seeking. When you construct std::istringstream iss(src), the get pointer starts at position 0 automatically. An initial seekg(0) is a no-op; it does not cause harm but is unnecessary noise.

Assuming locale independence without enforcing it. If user code ever calls std::locale::global(some_locale), all subsequently constructed string streams will use that locale. For serialization or protocol output where locale must not vary, always imbue(std::locale::classic()) explicitly.

std::istringstream for Parsing

std::istringstream is the idiomatic way to parse structured text in C++. It wraps a std::string buffer and exposes the full istream interface, so operator>>, std::getline, and format flags all apply without touching a file descriptor.

Parsing a CSV-like line with mixed types uses extraction chained with comma sentinels:

cpp
// C++11
#include <sstream>
#include <string>

void parse_csv_line() {
    std::istringstream iss("42,3.14,hello");
    int i;
    double d;
    std::string s;
    char comma;
    iss >> i >> comma >> d >> comma >> s;
    // i == 42, d == 3.14, s == "hello"
}

For delimiter-based splitting where fields may contain whitespace, std::getline with a delimiter argument is cleaner than operator>>:

cpp
// C++11
#include <sstream>
#include <string>
#include <vector>

std::vector<std::string> split_csv(const std::string& line) {
    std::istringstream iss(line);
    std::vector<std::string> fields;
    std::string token;
    while (std::getline(iss, token, ',')) {
        fields.push_back(token);
    }
    return fields;
}

After extraction, always check iss.fail() to detect a type mismatch rather than assuming success. std::from_chars (C++17) is faster for scalar parsing because it is locale-independent and allocation-free, but it requires manual range management and does not support compound parsing in a single expression.

Splitting Strings with istringstream

The canonical whitespace-split idiom feeds a string into an istringstream and extracts tokens with operator>>. Because operator>> skips leading whitespace before each extraction, the loop handles any run of spaces, tabs, or newlines transparently.

cpp
// C++11
#include <sstream>
#include <string>
#include <vector>

std::vector<std::string> split_words(const std::string& str) {
    std::istringstream iss(str);
    std::vector<std::string> words;
    std::string word;
    while (iss >> word) {
        words.push_back(word);
    }
    return words;
}
// split_words("  foo  bar\tbaz\n") β†’ {"foo", "bar", "baz"}

For splitting on an arbitrary single-character delimiter, replace operator>> with std::getline β€” it does not skip whitespace and consumes exactly one delimiter per call:

cpp
// C++11
#include <sstream>
#include <string>
#include <vector>

std::vector<std::string> split(const std::string& str, char delim) {
    std::istringstream iss(str);
    std::vector<std::string> tokens;
    std::string token;
    while (std::getline(iss, token, delim)) {
        tokens.push_back(token);
    }
    return tokens;
}

The key difference: operator>> treats all whitespace as a delimiter and skips empty segments, while std::getline(iss, token, delim) preserves empty fields between adjacent delimiters. C++20 introduces std::ranges::views::split as a lazy, allocation-deferred alternative:

cpp
// C++20
#include <ranges>
#include <string_view>

auto lazy_split(std::string_view sv, char delim) {
    return sv | std::views::split(delim);
}

ostringstream as a String Builder

std::ostringstream acts as a growable string buffer with full stream formatting support. Accumulating pieces with operator<< is the traditional technique for building strings from heterogeneous values:

cpp
// C++11
#include <sstream>
#include <string>

std::string build_log_line(const std::string& name, int value, double ratio) {
    std::ostringstream oss;
    oss << name << ": " << value << " (" << ratio << ")\n";
    return oss.str();   // str() copies the internal buffer
}

Call str() exactly once and store the result β€” each call copies the entire internal buffer. To reuse an ostringstream across multiple iterations, reset both the buffer content and the error state:

cpp
// C++11
#include <sstream>
#include <vector>
#include <string>

void build_many(const std::vector<int>& values) {
    std::ostringstream oss;
    for (int v : values) {
        oss.str("");    // discard buffer content
        oss.clear();    // clear eofbit / failbit
        oss << "item_" << v;
        // use oss.str() here
    }
}

ostringstream participates in locale formatting: numeric output is subject to the stream's locale, which can inject grouping separators or non-ASCII decimal points. This makes it measurably slower than plain std::string += for simple concatenation. For new code that requires formatting, prefer std::format (C++20) β€” it is stateless, does not allocate a stringbuf, and compiles format strings at compile time:

cpp
// C++20
#include <format>
#include <string>

std::string line = std::format("{}: {} ({:.2f})\n", name, value, ratio);

std::stringstream for In-Memory Protocol Encoding

std::stringstream can serve as an in-memory byte buffer for simple binary protocol encoding. Writing raw bytes via write() and reading them back with read() treats the stream as an unformatted byte channel; str() then exposes the encoded payload as a std::string.

cpp
// C++11
#include <sstream>
#include <cstdint>
#include <string>
#include <cstring>

std::string encode_message(uint32_t id, uint32_t length) {
    std::stringstream ss;
    // Write fields as big-endian raw bytes
    uint32_t id_be     = __builtin_bswap32(id);
    uint32_t length_be = __builtin_bswap32(length);
    ss.write(reinterpret_cast<const char*>(&id_be),     sizeof(id_be));
    ss.write(reinterpret_cast<const char*>(&length_be), sizeof(length_be));
    return ss.str();   // 8-byte encoded header
}

void decode_message(const std::string& bytes, uint32_t& id, uint32_t& length) {
    std::istringstream iss(bytes);
    uint32_t id_be, length_be;
    iss.read(reinterpret_cast<char*>(&id_be),     sizeof(id_be));
    iss.read(reinterpret_cast<char*>(&length_be), sizeof(length_be));
    id     = __builtin_bswap32(id_be);
    length = __builtin_bswap32(length_be);
}

This approach is convenient but carries overhead: str() copies the internal buffer on every call, and stringstream performs bookkeeping for stream state flags that binary use-cases do not need. A std::vector<char> with direct memcpy / pointer arithmetic is faster and more explicit for performance-critical binary I/O, and std::span<std::byte> (C++20) makes intent clearer still. Reserve stringstream binary encoding for prototypes or test harnesses.

wstringstream and Wide Characters

std::wostringstream and std::wistringstream provide the same stream interface over wchar_t buffers. Their primary use case is producing or consuming std::wstring values required by platform APIs β€” particularly the Windows API, which is wchar_t-native.

cpp
// C++11
#include <sstream>
#include <string>

std::wstring build_wide_label(const wchar_t* prefix, int value) {
    std::wostringstream woss;
    woss << prefix << L": " << value << L'\n';
    return woss.str();
}

void parse_wide_data(const std::wstring& data) {
    std::wistringstream wiss(data);
    std::wstring token;
    while (wiss >> token) {
        // process each wide token
    }
}

Mixing narrow and wide streams is not possible β€” a std::ostream and a std::wostream share no common base that supports formatted I/O, so you cannot insert std::string directly into a std::wostringstream. Use std::wstring literals (L"...") and wchar_t character literals throughout. The std::codecvt facet family (deprecated in C++17) was the standard mechanism for narrow ↔ wide conversion; modern code should prefer platform APIs (MultiByteToWideChar on Windows) or third-party libraries such as ICU. std::format gained wchar_t support in C++20 as std::wformat, providing a locale-independent wide formatting path:

cpp
// C++20
#include <format>
#include <string>

std::wstring wide_line = std::wformat(L"value: {}\n", 42);

Note that std::codecvt deprecation (C++17) does not remove it from the standard library; compilers continue to ship it, but its use is discouraged in new code.

sstream vs std::format vs from_chars: When to Use Each

Choosing the right tool for string formatting or parsing has measurable impact on both correctness and performance. The following table summarises the trade-offs across the four main options:

FacilityLocale-independentCompile-time checkAllocationsRelative speedStream composition
std::ostringstreamNo (inherits locale)Nostringbuf per objectBaselineYes
std::format (C++20)YesYes (format string)One std::string2–5Γ— fasterNo
std::to_stringNoNoOne std::stringSimilar to sstreamNo
std::to_chars (C++17)YesNoNone (caller-supplied buffer)5–10Γ— fasterNo

For new code that formats values into strings, prefer std::format:

cpp
// C++20
#include <format>
std::string s = std::format("x={}, y={:.3f}", x, y);

For performance-critical paths such as serializers, log formatters, or network protocol encoders, use std::to_chars / std::from_chars (C++17) β€” they are locale-independent, exception-free, and operate on a caller-supplied char array with no heap allocation:

cpp
// C++17
#include <charconv>
#include <array>

std::array<char, 32> buf;
auto [ptr, ec] = std::to_chars(buf.data(), buf.data() + buf.size(), 3.14);
// ec == std::errc{} on success; [buf.data(), ptr) holds the digits

Retain std::ostringstream / std::istringstream for legacy code that already depends on the stream insertion/extraction interface, for operator<< / operator>> unit testing, and for cases where the stream composition model (chaining manipulators, locale-imbued formatting, getline with custom delimiters) provides genuine value that std::format does not yet cover.

See Also

  • <charconv> β€” locale-independent, zero-allocation std::to_chars / std::from_chars (C++17)
  • <format> β€” type-safe formatted string construction via std::format (C++20)
  • <fstream> β€” file-backed streams sharing the same operator<</>> interface
  • <iomanip> β€” format manipulators: setw, setfill, hex, fixed, setprecision
  • <ios> β€” stream state flags (goodbit, eofbit, failbit, badbit) and clear()