Polyglot
Languages C++ io
C++ § io

I/O streams

The principal I/O facility in C++ is the iostreams library: a hierarchy of stream classes that admit insertion (<<) and extraction (>>) operators, manipulators for formatting, and file and string variants. The library has been part of C++ since C++98 and remains the conventional choice for stream-based I/O. C++17 added the <filesystem> library for path-and-directory operations; C++20 added std::format and C++23 added <print> as modern formatting interfaces, each with cleaner type safety and better performance than the iostream formatting surface. This page covers iostreams, the file and string variants, the manipulators, the modern formatting interfaces, and the filesystem library.

The stream hierarchy

The principal classes:

std::ios_base
└── std::basic_ios<CharT>
    ├── std::basic_istream<CharT>
    │   ├── std::basic_ifstream<CharT>
    │   ├── std::basic_istringstream<CharT>
    │   └── std::basic_iostream<CharT> (also derives from basic_ostream)
    └── std::basic_ostream<CharT>
        ├── std::basic_ofstream<CharT>
        ├── std::basic_ostringstream<CharT>
        └── std::basic_iostream<CharT>

Each class is a template instantiation; the conventional aliases use char:

Template instantiationConventional alias
std::basic_istream<char>std::istream
std::basic_ostream<char>std::ostream
std::basic_iostream<char>std::iostream
std::basic_ifstream<char>std::ifstream
std::basic_ofstream<char>std::ofstream
std::basic_fstream<char>std::fstream
std::basic_istringstream<char>std::istringstream
std::basic_ostringstream<char>std::ostringstream
std::basic_stringstream<char>std::stringstream

For wide-character streams, the corresponding std::wistream, std::wostream, etc., wrap wchar_t-based streams.

The standard streams

Four streams are available without explicit construction, declared in <iostream>:

  • std::cin — standard input. An std::istream.
  • std::cout — standard output. An std::ostream.
  • std::cerr — standard error. An std::ostream. Conventionally unbuffered.
  • std::clog — standard error, buffered. An std::ostream.
#include <iostream>

int main() {
    std::cout << "enter your name: ";
    std::string name;
    std::cin >> name;
    std::cout << "hello, " << name << '\n';
}

The wide-character analogues are std::wcin, std::wcout, std::wcerr, std::wclog.

Insertion and extraction

The fundamental operators are << (insertion, for output) and >> (extraction, for input):

std::cout << 42;            // int
std::cout << 3.14;          // double
std::cout << "hello";       // const char *
std::cout << std::string{"world"};
std::cout << '\n';          // char

int x;
std::cin >> x;              // reads an int

std::string word;
std::cin >> word;           // reads a whitespace-delimited word

double d;
std::cin >> d;              // reads a double

Both operators return a reference to the stream, which is the mechanism by which they chain:

std::cout << "x = " << x << ", y = " << y << '\n';

For user-defined types, the conventional pattern is to overload operator<< and operator>> as non-member functions taking the stream by reference:

struct Point { double x, y; };

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

std::istream &operator>>(std::istream &is, Point &p) {
    char open, comma, close;
    return is >> open >> p.x >> comma >> p.y >> close;
}

Point p{3.0, 4.0};
std::cout << p << '\n';     // (3, 4)

Returning the stream by reference admits the chaining; taking the user-defined type by const reference (for output) or non-const reference (for input) follows the conventional pattern.

Manipulators

Manipulators modify stream state. The most common are declared in <ios>, <iomanip>, and <iostream>:

ManipulatorEffect
std::endlInsert \n and flush.
std::flushFlush.
std::ws(Input) Skip whitespace.
std::boolalpha, std::noboolalphaPrint bool as true/false or 1/0.
std::dec, std::oct, std::hexInteger base for input/output.
std::fixed, std::scientificFloating-point format.
std::left, std::right, std::internalField alignment.
std::showpoint, std::noshowpointAlways show decimal point.
std::showpos, std::noshowposShow + for positive numbers.
std::uppercase, std::nouppercaseUppercase hex digits and exponent character.

The parameterised manipulators in <iomanip>:

ManipulatorEffect
std::setw(n)Field width for the next insertion.
std::setfill(c)Padding character.
std::setprecision(n)Floating-point precision.
std::setbase(b)Integer base (2, 8, 10, 16).
#include <iomanip>

std::cout << std::fixed << std::setprecision(2)
          << std::setw(10) << std::setfill(' ')
          << 3.14159 << '\n';
//      3.14

std::cout << std::hex << std::showbase << 255 << '\n';
// 0xff

Most manipulators set persistent state on the stream; once changed, the new state applies to subsequent insertions. The exception is std::setw, which applies only to the next insertion.

The interaction of stream state with formatting is one of the principal awkwardnesses of the iostream library; this is part of why std::format and <print> are the conventional contemporary choice.

std::endl versus '\n'

std::endl inserts a newline and flushes the stream. '\n' inserts only the newline; flushing is left to the buffering rules. The flush imposes a non-trivial cost; the conventional advice is to use '\n' unless the flush is genuinely needed:

std::cout << "line\n";          // efficient
std::cout << "line" << std::endl;  // inserts \n and flushes

Output to std::cerr is unbuffered, so std::endl and '\n' are equivalent for it.

File streams

The <fstream> header provides three classes:

  • std::ifstream — read from a file.
  • std::ofstream — write to a file.
  • std::fstream — read and write.
#include <fstream>

void read_file(const std::string &path) {
    std::ifstream in(path);
    if (!in) {
        std::cerr << "cannot open " << path << '\n';
        return;
    }

    std::string line;
    while (std::getline(in, line)) {
        process(line);
    }
}

void write_file(const std::string &path, const std::vector<int> &data) {
    std::ofstream out(path);
    if (!out) {
        std::cerr << "cannot open " << path << '\n';
        return;
    }

    for (int x : data) out << x << '\n';
}

The file is opened by the constructor (or by an explicit open() call) and closed by the destructor. The class is RAII: the file is always closed when the stream goes out of scope.

The mode arguments to the constructor or open():

ModeEffect
std::ios::inOpen for reading. (Default for ifstream.)
std::ios::outOpen for writing. (Default for ofstream.)
std::ios::appAppend.
std::ios::truncTruncate (default for out without app).
std::ios::binaryBinary mode.
std::ios::atePosition at the end on open.
std::ofstream out("data.bin", std::ios::binary);
std::ofstream log("app.log",  std::ios::app);

Modes may be combined with |:

std::fstream file("data.bin", std::ios::in | std::ios::out | std::ios::binary);

String streams

The <sstream> header provides three classes that wrap a std::string:

  • std::istringstream — read from a string.
  • std::ostringstream — write to a string.
  • std::stringstream — read and write.
#include <sstream>

std::string format_point(double x, double y) {
    std::ostringstream out;
    out << '(' << x << ", " << y << ')';
    return out.str();
}

std::vector<int> parse_numbers(const std::string &input) {
    std::istringstream in(input);
    std::vector<int>   result;
    int                value;
    while (in >> value) result.push_back(value);
    return result;
}

String streams are the conventional way to build formatted strings or to parse formatted input. They have been substantially superseded by std::format for output (which is faster, type-safer, and more readable) and by std::from_chars for parsing (which is faster and does not allocate). The classes remain useful for incremental I/O and for code that must work with stream-based interfaces.

Stream state and error reporting

A stream has four state bits:

BitSet when
goodbitNo bits are set; everything is fine.
eofbitEnd of input has been reached.
failbitAn operation failed (parse error, format mismatch).
badbitA serious error occurred (I/O failure).

The conventional tests:

if (in)               // good
if (!in)              // failbit or badbit
if (in.eof())         // eofbit
if (in.fail())        // failbit or badbit
if (in.bad())         // badbit
if (in.good())        // none set

A stream’s bool conversion (if (in)) tests !fail(). The convention while (in >> x) reads until the stream fails (EOF, format mismatch, or I/O error).

clear() resets the state bits; useful after recovering from a parse error.

Synchronised I/O

By default, the C++ streams are synchronised with the C streams (stdin, stdout, stderr). The synchronisation imposes a non-trivial cost; for performance-critical I/O the synchronisation may be disabled:

std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);              // also break the cin↔cout flush coupling

The settings are conventional in competitive-programming code where I/O throughput is the bottleneck; for ordinary applications the synchronisation cost is rarely meaningful.

The <filesystem> library

C++17 added <filesystem> for path-and-directory operations:

#include <filesystem>

namespace fs = std::filesystem;

fs::path p = "/etc/passwd";
if (fs::exists(p) && fs::is_regular_file(p)) {
    auto sz = fs::file_size(p);
    std::cout << p << ": " << sz << " bytes\n";
}

for (const auto &entry : fs::directory_iterator("/var/log")) {
    std::cout << entry.path() << '\n';
}

fs::copy_file("source.txt", "destination.txt", fs::copy_options::overwrite_existing);
fs::remove("temporary.dat");
fs::create_directories("a/b/c");

The library covers:

  • fs::path — the cross-platform path type.
  • fs::exists, fs::is_regular_file, fs::is_directory, fs::is_symlink — file metadata.
  • fs::file_size, fs::last_write_time, fs::status — more metadata.
  • fs::directory_iterator, fs::recursive_directory_iterator — directory traversal.
  • fs::copy, fs::copy_file, fs::rename, fs::remove, fs::create_directory, fs::create_directories — modifications.
  • fs::current_path, fs::temp_directory_path, fs::canonical, fs::absolute — path manipulation.

The library abstracts over Unix and Windows filesystem APIs; portable code uses it in preference to the platform-specific calls.

std::format and <print> as the modern interface

C++20’s std::format is the modern formatting interface; C++23’s <print> adds direct output:

#include <format>
#include <print>

auto s = std::format("{} is {} years old\n", name, age);

std::print("{} is {} years old\n", name, age);
std::println("done.");

The format-string mini-language admits:

DirectiveMeaning
{}Default format for the argument.
{:.2f}Floating-point with two decimal places.
{:10}Field width 10 (right-aligned).
{:<10}Left-aligned.
{:^10}Centred.
{:#x}Hexadecimal with 0x prefix.
{:#08x}Hexadecimal, padded to 8 digits with leading zeros.
{0}, {1}, {0}Positional arguments (re-use).

The format string is checked at compile time when it is a constexpr. The library is substantially faster than <iostream> for typical workloads and produces more concise code.

The conventional contemporary advice:

  • Use std::format for string construction.
  • Use std::print / std::println for direct output.
  • Use iostreams when integrating with stream-based code.
  • Use printf-family C functions only at the C-interoperability boundary.

Common patterns

Read an entire file into a string

std::string read_file(const std::string &path) {
    std::ifstream     in(path, std::ios::binary);
    std::ostringstream contents;
    contents << in.rdbuf();
    return contents.str();
}

The rdbuf() access is the idiomatic form: insert the entire stream buffer into the string stream’s buffer.

Read line by line

std::string line;
while (std::getline(in, line)) {
    process(line);
}

std::getline reads up to the next newline (excluding it) into line. It admits a third argument for a custom delimiter:

while (std::getline(in, field, ',')) {
    /* one comma-delimited field */
}

Write fixed-format records

std::ofstream out(path, std::ios::binary);
for (const auto &record : records) {
    out.write(reinterpret_cast<const char*>(&record), sizeof(record));
}

write is the binary-mode counterpart to <<. The cast to const char* is well-defined because all object types may be aliased through char*.

For non-trivially-copyable records, individual-field serialisation is required; the cast-and-write pattern works only for trivially-copyable types.

Buffered reading

For high-throughput I/O, reading into a buffer and parsing the buffer is faster than reading element by element:

constexpr size_t BUFFER_SIZE = 65536;
char             buffer[BUFFER_SIZE];

while (in.read(buffer, BUFFER_SIZE) || in.gcount() > 0) {
    process(buffer, in.gcount());
}

read returns the stream; gcount() returns the number of characters actually read.

Redirecting output

A stream’s underlying buffer can be redirected to another stream’s buffer:

std::ofstream log("app.log");
auto         original_buffer = std::cout.rdbuf();
std::cout.rdbuf(log.rdbuf());

std::cout << "this goes to the log file\n";

std::cout.rdbuf(original_buffer);   // restore

The mechanism is occasionally useful for diagnostic capture and for testing.

A note on the alternatives

Beyond <iostream> and <print>, C++ admits the C <cstdio> family (std::printf, std::scanf) and platform-specific I/O. The conventional discipline is:

  • For portable, type-safe formatted output: std::format and <print>.
  • For stream-based I/O: <iostream> and the file/string stream classes.
  • For high-throughput binary I/O: <fstream> with read/write and a buffer.
  • For platform-specific or specialised use: <cstdio>, POSIX <unistd.h>, or platform APIs.

The combination of <iostream> (for stream-based code) and <format>/<print> (for formatted output) covers the substantial majority of C++ I/O. The transition from iostreams to the new formatting interfaces is gradual; new code should use std::format and std::print where they apply, with iostreams as the fallback for stream-style code that already exists.