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 instantiation | Conventional 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. Anstd::istream.std::cout— standard output. Anstd::ostream.std::cerr— standard error. Anstd::ostream. Conventionally unbuffered.std::clog— standard error, buffered. Anstd::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>:
| Manipulator | Effect |
|---|---|
std::endl | Insert \n and flush. |
std::flush | Flush. |
std::ws | (Input) Skip whitespace. |
std::boolalpha, std::noboolalpha | Print bool as true/false or 1/0. |
std::dec, std::oct, std::hex | Integer base for input/output. |
std::fixed, std::scientific | Floating-point format. |
std::left, std::right, std::internal | Field alignment. |
std::showpoint, std::noshowpoint | Always show decimal point. |
std::showpos, std::noshowpos | Show + for positive numbers. |
std::uppercase, std::nouppercase | Uppercase hex digits and exponent character. |
The parameterised manipulators in <iomanip>:
| Manipulator | Effect |
|---|---|
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():
| Mode | Effect |
|---|---|
std::ios::in | Open for reading. (Default for ifstream.) |
std::ios::out | Open for writing. (Default for ofstream.) |
std::ios::app | Append. |
std::ios::trunc | Truncate (default for out without app). |
std::ios::binary | Binary mode. |
std::ios::ate | Position 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:
| Bit | Set when |
|---|---|
goodbit | No bits are set; everything is fine. |
eofbit | End of input has been reached. |
failbit | An operation failed (parse error, format mismatch). |
badbit | A 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:
| Directive | Meaning |
|---|---|
{} | 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::formatfor string construction. - Use
std::print/std::printlnfor 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::formatand<print>. - For stream-based I/O:
<iostream>and the file/string stream classes. - For high-throughput binary I/O:
<fstream>withread/writeand 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.