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

iostreams — Input/Output Streams

"C++ iostream system: standard streams, file and string streams, stream state, manipulators, custom operators, rdbuf redirection, and performance tuning."

iostreamsince C++98

The iostream library provides buffered, type-safe, extensible stream I/O through a hierarchy of class templates rooted in std::basic_ios, supporting console, file, string, and (since C++23) span-backed streams through a unified interface.

Overview

Three orthogonal axes define the iostream system:

  • Direction: istream (input), ostream (output), iostream (bidirectional)
  • Backing storage: console (cin/cout/cerr/clog), file (ifstream/ofstream/fstream), in-memory string (istringstream/ostringstream/stringstream), or since C++23 a std::span<char> (ispanstream/ospanstream)
  • Character type: char (std::ostream) or wchar_t (std::wostream); all classes are typedefs over basic_ostream<CharT, Traits>

The extraction (>>) and insertion (<<) operators are overloaded free functions that ultimately delegate to read/write on a std::streambuf. This means the same formatting code works with any backing storage, and you can swap backends at runtime via rdbuf().

Standard stream characteristics:

  • std::cout — buffered standard output; tied to cin by default
  • std::cerr — unbuffered standard error; every write is an immediate syscall
  • std::clog — buffered standard error; prefer for diagnostic output
  • std::cin — standard input; by default tied to cout, flushing it before each read

Syntax

cpp
#include <iostream>    // cin, cout, cerr, clog
#include <fstream>     // ifstream, ofstream, fstream
#include <sstream>     // istringstream, ostringstream, stringstream
#include <iomanip>     // setw, setprecision, setfill, hex, etc.
#include <spanstream>  // C++23: ispanstream, ospanstream

Examples

Standard I/O and Stream State

cpp
#include <iostream>
#include <limits>

int read_int(const char* prompt) {
    int val;
    while (true) {
        std::cerr << prompt;
        if (std::cin >> val)
            return val;
        // failbit is set — clear it before attempting to read again
        std::cin.clear();
        std::cin.ignore(
            std::numeric_limits<std::streamsize>::max(), '\n');
    }
}

Four state bits: good(), fail(), eof(), bad(). A failed extraction sets failbit; bad() signals an unrecoverable I/O error on the underlying device. Call clear() before ignore() — ignore() refuses to read from a stream already in a failed state.

Enable exceptions per-stream if you prefer RAII-style error handling:

cpp
std::ifstream in{"data.csv"};
in.exceptions(std::ios::failbit | std::ios::badbit);
// Any failed read now throws std::ios::failure instead of silently setting bits
std::string line;
while (std::getline(in, line)) { /* ... */ }

File Streams

cpp
#include <fstream>
#include <stdexcept>
#include <cstdint>   // C++11: uint32_t

// Text append — RAII close on destruction
void append_log(const std::string& msg) {
    std::ofstream out{"app.log", std::ios::app};
    if (!out) throw std::runtime_error{"cannot open log"};
    out << msg << '\n';   // '\n' — never use endl in a loop
}

// Read entire file as bytes (C++11: istreambuf_iterator constructor)
std::string slurp(const char* path) {
    std::ifstream in{path, std::ios::binary};
    if (!in) throw std::runtime_error{"cannot open file"};
    return {std::istreambuf_iterator<char>{in},
            std::istreambuf_iterator<char>{}};
}

// Seekable random access
void patch_magic(const char* path, std::uint32_t magic) {
    std::fstream f{path, std::ios::in | std::ios::out | std::ios::binary};
    f.seekp(0);                                              // write position
    f.write(reinterpret_cast<const char*>(&magic), sizeof magic);

    f.seekg(0, std::ios::end);                               // read position
    std::streamsize size = f.tellg();                        // file size in bytes
}

Opening ofstream without std::ios::app truncates silently. To open for write without truncation, use fstream with std::ios::in | std::ios::out.

String Streams

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

// C++20+: std::format is faster, locale-independent, and clearer
std::string fmt_record(int id, double val) {
    return std::format("id={} val={:.4f}", id, val);  // C++20
}

// Pre-C++20 pattern when you need runtime formatting into a string
std::string fmt_record_legacy(int id, double val) {
    std::ostringstream oss;
    oss << "id=" << id << " val=" << std::fixed << std::setprecision(4) << val;
    return oss.str();
}

// istringstream as a lightweight line parser
std::vector<double> parse_row(const std::string& csv_line) {
    std::istringstream iss{csv_line};
    std::vector<double> row;
    double v;
    while (iss >> v) {
        row.push_back(v);
        char comma;
        iss >> comma;  // consume optional ','
    }
    return row;
}

Manipulators

cpp
#include <iomanip>

void demo_formatting() {
    // Integer bases — sticky
    std::cout << std::hex << std::uppercase << std::showbase;
    std::cout << 255 << '\n';     // "0XFF"
    std::cout << std::dec;        // reset to decimal

    // setw is NOT sticky — resets to 0 after every formatted output
    std::cout << std::setfill('0') << std::setw(8) << 42 << '\n';  // "00000042"
    std::cout << std::setw(8) << 1 << 2 << '\n';                   // "       12", not "       1       2"

    // Floats — sticky until changed
    std::cout << std::fixed      << std::setprecision(3) << 3.14159 << '\n'; // "3.142"
    std::cout << std::scientific << 0.000123 << '\n';                         // "1.230e-04"
    std::cout << std::defaultfloat;  // reset to default representation

    // Bool (C++98: outputs 0/1 by default)
    std::cout << std::boolalpha << true << '\n';   // "true"
    std::cout << std::noboolalpha << true << '\n'; // "1"
}

Custom Stream Operators

cpp
struct Vec3 { float x, y, z; };

std::ostream& operator<<(std::ostream& os, const Vec3& v) {
    return os << '(' << v.x << ", " << v.y << ", " << v.z << ')';
}

std::istream& operator>>(std::istream& is, Vec3& v) {
    char c;
    if (is >> c && c == '(')
        if (is >> v.x >> c && c == ',')
            if (is >> v.y >> c && c == ',')
                if (is >> v.z >> c && c == ')')
                    return is;
    is.setstate(std::ios::failbit);
    return is;
}

Chain state checks rather than throwing inside operator>>; let the caller decide whether to check state or enable exceptions via is.exceptions(std::ios::failbit).

Redirecting Streams via rdbuf

cpp
#include <sstream>
#include <iostream>

// Capture stdout in tests without spawning a subprocess
struct CaptureStdout {
    std::ostringstream buf;
    std::streambuf* prev;

    CaptureStdout() : prev{std::cout.rdbuf(buf.rdbuf())} {}
    ~CaptureStdout() { std::cout.rdbuf(prev); }

    std::string str() const { return buf.str(); }
};

void test_printer() {
    CaptureStdout cap;
    std::cout << "hello " << 42;
    assert(cap.str() == "hello 42");
}

C++23: spanstream

cpp
#include <spanstream>  // C++23

char buf[64];
std::ospanstream out{std::span<char>{buf}};
out << "x=" << 42 << " y=" << 3.14f;
auto written = out.span();  // std::span<char> covering only the written bytes
std::string_view sv{written.data(), written.size()};
// No heap allocation — useful in embedded or latency-sensitive contexts

Best Practices

  • '\n' over std::endl — endl forces a buffer flush on every call; in any loop this destroys throughput.
  • Prefer std::format/std::print (C++20/23) for all new formatting code. Use iostreams for the I/O backend, not as a formatting engine.
  • Disable C/C++ stream sync for bulk I/O:
    cpp
    int main() {
        std::ios::sync_with_stdio(false);  // decouple from stdio
        std::cin.tie(nullptr);             // don't auto-flush cout before cin reads
        // Do NOT mix printf/scanf with cout/cin after this point
    }
  • Always use std::ios::binary for binary files. On Windows, text mode translates \r\n ↔ \n, silently corrupting non-text data.
  • Use std::filesystem::path (C++17) for file paths to handle non-ASCII filenames portably across platforms.
  • Prefer std::clog over std::cerr for high-rate diagnostics — cerr is unbuffered and incurs one syscall per write.

Common Pitfalls

setw resets after each output. Unlike every other manipulator, width does not persist:

cpp
std::cout << std::setw(6) << 1 << 2;  // "     12", NOT "     1     2"

Mixing >> with getline leaves a newline in the buffer:

cpp
int n;
std::cin >> n;                    // consumes digits, leaves '\n' in buffer
std::string line;
std::getline(std::cin, line);     // reads that '\n' immediately — line is empty
// Fix: discard the leftover newline first
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::getline(std::cin, line);

istreambuf_iterator reads raw bytes. On Windows text-mode streams, \r\n pairs are collapsed before you see them. Open with std::ios::binary when you need byte-exact content.

Exceptions thrown inside operator>> skip the setstate call, leaving the stream appearing healthy while the value is garbage. Set failbit explicitly and let exceptions propagate from the state check, or use is.exceptions() to turn state flags into exceptions automatically.

Stream state flags and error handling

Every stream carries four state bits that must be checked after any I/O. The bits are goodbit (no error), failbit (logical error — bad format), badbit (unrecoverable hardware/buffer error), and eofbit (end of input reached).

cpp
#include <iostream>
#include <fstream>

void state_demo() {
    std::ifstream f{"data.bin", std::ios::binary};

    int x;
    f >> x;

    if (f.good())   { /* all OK */          }
    if (f.fail())   { /* failbit or badbit */ }
    if (f.eof())    { /* eofbit set         */ }
    if (f.bad())    { /* badbit: corrupted   */ }
    if (!f)         { /* same as f.fail()   */ }

    // Reset recoverable errors before retrying
    f.clear();                       // clears all bits → goodbit
    f.clear(std::ios::eofbit);       // clear fail/bad, keep eof
}

Calling clear() without arguments resets all bits to goodbit. To set a specific bit without clearing others, use setstate():

cpp
// Signal parse failure in operator>> without throwing
is.setstate(std::ios::failbit);

// Or combine: mark EOF + fail simultaneously
is.setstate(std::ios::failbit | std::ios::eofbit);

To opt into exceptions rather than polling bits, call exceptions() once after opening the stream. std::ios::failure derives from std::system_error since C++11, so e.code() carries the OS error code on badbit.

cpp
std::ifstream in{"report.csv"};
in.exceptions(std::ios::failbit | std::ios::badbit); // C++11
try {
    std::string line;
    while (std::getline(in, line)) { /* process */ }
} catch (const std::ios::failure& e) {
    // e.what() + e.code() describe the failure
}

Custom stream manipulators

A manipulator is simply a function (or callable) that accepts a stream reference and returns it. For stateless manipulators, a plain function that takes std::ostream& and returns std::ostream& is enough.

cpp
#include <iostream>

// Stateless manipulator: emit a hard separator line
std::ostream& separator(std::ostream& os) {
    return os << "\n--- --- ---\n";
}

std::cout << "Section A" << separator << "Section B\n";
// Output:
// Section A
// --- --- ---
// Section B

For parametric manipulators, return a helper struct that overloads operator<<:

cpp
#include <iostream>
#include <iomanip>

struct Repeat {
    char ch;
    int  n;
};

std::ostream& operator<<(std::ostream& os, Repeat r) {
    for (int i = 0; i < r.n; ++i) os.put(r.ch);
    return os;
}

// Usage: factory helper keeps call-site readable
constexpr Repeat repeat(char ch, int n) { return {ch, n}; }

std::cout << repeat('=', 40) << '\n';
// Output: ========================================

Sticky state (like std::hex) is stored in the stream's fmtflags. Use os.flags() to save/restore the entire flag set when your manipulator changes formatting and you need to leave the stream unchanged for the caller:

cpp
std::ostream& hex_dump_byte(std::ostream& os, unsigned char b) {
    auto saved = os.flags();              // save current flags
    os << std::hex << std::uppercase
       << std::setfill('0') << std::setw(2) << static_cast<int>(b);
    os.flags(saved);                      // restore — leaves hex mode off
    return os;
}

Overloading operator<< and operator>>

Overload these operators as non-member free functions, not as member functions, so the stream is the left-hand operand and the type integrates naturally into chained expressions.

cpp
#include <iostream>

struct Color { uint8_t r, g, b; };

// Output: always takes const ref, always returns the stream
std::ostream& operator<<(std::ostream& os, const Color& c) {
    auto flags = os.flags();
    os << '#' << std::hex << std::uppercase << std::setfill('0')
       << std::setw(2) << +c.r
       << std::setw(2) << +c.g
       << std::setw(2) << +c.b;
    os.flags(flags);
    return os;
}

// Input: takes non-const ref, sets failbit on parse error
std::istream& operator>>(std::istream& is, Color& c) {
    char hash;
    unsigned int rgb = 0;
    if (is >> hash && hash == '#' && is >> std::hex >> rgb) {
        c.r = (rgb >> 16) & 0xFF;
        c.g = (rgb >>  8) & 0xFF;
        c.b =  rgb        & 0xFF;
    } else {
        is.setstate(std::ios::failbit);
    }
    is >> std::dec;  // restore decimal for subsequent reads
    return is;
}

For operator>>, always restore any format flags you change (here std::hex → std::dec), and always test the stream state before writing to the output object. Accessing c when the stream is already failed results in leaving c in an indeterminate state.

Wide streams (wchar_t)

Every narrow stream type has a w-prefixed wide counterpart backed by wchar_t. The type aliases are std::wcin, std::wcout, std::wcerr, std::wclog, and the class templates std::wifstream, std::wofstream, std::wostringstream, etc.

cpp
#include <iostream>
#include <fstream>
#include <locale>

void wide_demo() {
    // Wide console I/O
    std::wcout << L"Héllo Wörld\n";   // wchar_t string literal

    // Write UTF-16 / wide text to file using a locale with a codecvt facet
    std::wofstream wf{"output.txt"};
    wf.imbue(std::locale("en_US.UTF-8"));
    wf << L"αβγδ\n";
}

Mixing narrow and wide streams on the same standard stream handle (stdout) invokes undefined behavior. Pick one orientation per handle and stick with it for the lifetime of the program. The std::ios_base::sync_with_stdio(false) call does not affect wide streams' orientation — call std::locale::global() or imbue() to configure encoding instead.

cpp
// C++11: portable locale approach
#include <locale>

int main() {
    std::locale::global(std::locale(""));  // system locale  // C++11
    std::wcout.imbue(std::locale());
    std::wcout << L"élève\n";    // "élève"
}

Tying streams with tie()

tie() creates a dependency between two streams so the tied stream is flushed automatically before each I/O on the dependent stream. By default, std::cin is tied to std::cout, guaranteeing prompts appear before reads.

cpp
#include <iostream>

// Check what cin is currently tied to
std::ostream* old_tie = std::cin.tie();   // returns &std::cout by default

// Remove the automatic flush (performance optimization — see next section)
std::cin.tie(nullptr);

// Tie a log stream to cerr so every cerr write flushes the log first
std::ofstream log{"trace.log"};
std::cerr.tie(&log);

tie() returns the previously tied stream, allowing you to restore the original relationship. Use tie(nullptr) only when you control all I/O ordering in main and never need interleaved console prompts.

cpp
// Scoped tie guard
struct TieGuard {
    std::istream&  is;
    std::ostream*  prev;
    TieGuard(std::istream& is, std::ostream* to)
        : is{is}, prev{is.tie(to)} {}
    ~TieGuard() { is.tie(prev); }
};

// Example: temporarily disable auto-flush during bulk reads
TieGuard g{std::cin, nullptr};
for (int i = 0; i < 1'000'000; ++i) {
    int x; std::cin >> x;
}
// tie restored automatically

Performance: sync_with_stdio and untie

Two independent bottlenecks limit iostream throughput: synchronization with C stdio and automatic flushing via tie(). Disabling both can yield 5–20× improvement on bulk console or file I/O.

cpp
#include <iostream>

int main() {
    // Must be called before ANY I/O on standard streams
    std::ios::sync_with_stdio(false);  // C++98, but most useful in modern code
    std::cin.tie(nullptr);             // detach auto-flush of cout before cin reads

    // After this point: do NOT mix printf/scanf with cout/cin
    // They share the same file descriptors but no longer share buffers

    int n; std::cin >> n;
    for (int i = 0; i < n; ++i) {
        int x; std::cin >> x;
        std::cout << x * 2 << '\n';   // '\n', never std::endl
    }
}

Additional tunables for heavy file I/O:

cpp
// Increase streambuf internal buffer (non-standard but widely supported)
#include <fstream>
#include <array>

std::ifstream in{"bigfile.bin", std::ios::binary};
std::array<char, 65536> iobuf;
in.rdbuf()->pubsetbuf(iobuf.data(), iobuf.size());

// Alternative: read entire file in one syscall with istreambuf_iterator
std::string content{std::istreambuf_iterator<char>{in},
                    std::istreambuf_iterator<char>{}};

sync_with_stdio(false) has no effect on file streams — it only affects the five standard streams. The call is process-global and cannot be reversed after the first I/O operation.

iostream vs std::format (C++20)

std::format (C++20) separates formatting from I/O, producing a std::string without touching any stream. Use iostreams for the transport layer (file, socket, console) and std::format/std::print for the formatting layer.

cpp
#include <format>    // C++20
#include <print>     // C++23
#include <iostream>
#include <fstream>

// C++20: build the string, then write it
void log_entry_cpp20(std::ofstream& out, int id, double value) {
    out << std::format("id={:>6d}  val={:.4f}\n", id, value);  // C++20
}

// C++23: std::print writes directly to a stream — no intermediate string
void log_entry_cpp23(std::ofstream& out, int id, double value) {
    std::print(out, "id={:>6d}  val={:.4f}\n", id, value);     // C++23
}

// Comparison: ostringstream (pre-C++20 pattern, avoid in new code)
void log_entry_legacy(std::ofstream& out, int id, double value) {
    std::ostringstream oss;
    oss << "id=" << std::setw(6) << id
        << "  val=" << std::fixed << std::setprecision(4) << value << '\n';
    out << oss.str();
}

Key differences:

ostringstreamstd::format (C++20)std::print (C++23)
Format stringScattered manipulatorsInline {} specInline {} spec
LocaleAlways appliedOpt-in via {:L}Opt-in via {:L}
AllocationAlways heapOne stringNone (direct write)
Compile-time checkNoneYes (consteval)Yes (consteval)

Prefer std::print / std::println for all new console and log output. Reserve operator<< overloads for types that must interoperate with pre-C++20 code or with stream-based serialization frameworks.

See Also