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++98The 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 astd::span<char>(ispanstream/ospanstream) - Character type:
char(std::ostream) orwchar_t(std::wostream); all classes are typedefs overbasic_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 tocinby defaultstd::cerr— unbuffered standard error; every write is an immediate syscallstd::clog— buffered standard error; prefer for diagnostic outputstd::cin— standard input; by default tied tocout, flushing it before each read
Syntax
#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, ospanstreamExamples
Standard I/O and Stream State
#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:
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
#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
#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
#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
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
#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
#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 contextsBest Practices
'\n'overstd::endl—endlforces 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::binaryfor 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::clogoverstd::cerrfor high-rate diagnostics —cerris unbuffered and incurs one syscall per write.
Common Pitfalls
setw resets after each output. Unlike every other manipulator, width does not persist:
std::cout << std::setw(6) << 1 << 2; // " 12", NOT " 1 2"Mixing >> with getline leaves a newline in the buffer:
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).
#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():
// 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.
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.
#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 BFor parametric manipulators, return a helper struct that overloads operator<<:
#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:
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.
#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.
#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.
// 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.
#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.
// 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 automaticallyPerformance: 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.
#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:
// 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.
#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:
ostringstream | std::format (C++20) | std::print (C++23) | |
|---|---|---|---|
| Format string | Scattered manipulators | Inline {} spec | Inline {} spec |
| Locale | Always applied | Opt-in via {:L} | Opt-in via {:L} |
| Allocation | Always heap | One string | None (direct write) |
| Compile-time check | None | Yes (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
std::format— C++20 type-safe, locale-optional text formattingstd::print/std::println— C++23 formatted output directly to streamsstd::filesystem— C++17 portable path and file operationsstd::spanstream— C++23 fixed-buffer streams without heap allocation