fstream
File stream classes for reading, writing, and random-access I/O on files; the <fstream> header provides ifstream, ofstream, and fstream.
std::ifstream / std::ofstream / std::fstreamsince C++98The <fstream> header provides RAII-managed file stream classes β std::ifstream for reading, std::ofstream for writing, and std::fstream for both β all deriving from the same std::basic_ios hierarchy that backs std::cin and std::cout.
Overview
std::ifstream, std::ofstream, and std::fstream are type aliases for std::basic_ifstream<char>, std::basic_ofstream<char>, and std::basic_fstream<char>. Wide-character variants (std::wifstream, std::wofstream, std::wfstream) cover wchar_t. Because they inherit from std::istream, std::ostream, and std::iostream respectively, all formatted I/O operators (>>, <<) and unformatted member functions (get, read, getline, write) behave identically to their cin/cout equivalents.
The file is opened in the constructor and closed by the destructor. open()/close() are available when you need to reuse a stream variable across multiple files.
Open modes
| Flag | Effect |
|---|---|
ios::in | Open for reading (default for ifstream) |
ios::out | Open for writing (default for ofstream) |
ios::binary | Disable newline translation; raw byte I/O |
ios::trunc | Discard existing file contents on open |
ios::app | All writes go to end of file; position is not seekable |
ios::ate | Seek to end immediately after open; position remains seekable |
std::fstream defaults to ios::in | ios::out but will not create the file if it does not exist β unlike std::ofstream, which implies ios::out | ios::trunc and creates the file. Flags combine with |:
std::ofstream log("app.log", std::ios::out | std::ios::app);Syntax
#include <fstream>
// Construct with immediate open (C++98)
std::ifstream in("data.txt");
std::ofstream out("result.txt");
std::fstream rw("store.bin", std::ios::in | std::ios::out | std::ios::binary);
// C++17: accept std::filesystem::path
#include <filesystem>
std::ifstream in2(std::filesystem::path{"data"} / "input.txt"); // C++17
// Deferred open
std::ifstream in3;
in3.open("late.txt");
if (!in3) { /* failbit set β file missing or permission denied */ }
// Exception-based error handling (exceptions are off by default)
in.exceptions(std::ios::failbit | std::ios::badbit);
// Streams are movable but not copyable (C++11)
std::ifstream make_reader(const char* path) {
return std::ifstream(path); // move on return (C++11)
}Examples
Formatted text I/O
#include <fstream>
#include <iostream>
#include <string>
int main() {
{
std::ofstream out("numbers.txt");
if (!out) return 1;
out << 42 << ' ' << 3.14 << '\n';
} // destructor flushes and closes
std::ifstream in("numbers.txt");
if (!in) return 1;
int n; double d;
in >> n >> d;
std::cout << "int=" << n << " double=" << d << '\n';
}Reading all lines from a file
The correct idiom is while (std::getline(...)), not while (!eof()):
#include <fstream>
#include <string>
#include <vector>
std::vector<std::string> read_lines(const char* path) {
std::ifstream in(path);
if (!in) throw std::runtime_error(std::string("cannot open: ") + path);
std::vector<std::string> lines;
std::string line;
while (std::getline(in, line))
lines.push_back(std::move(line)); // C++11
return lines;
}Binary I/O with read and write
#include <fstream>
#include <cstdint>
#include <vector>
#include <stdexcept>
struct Record { // must be trivially copyable
std::uint32_t id;
double value;
};
void write_records(const char* path, const std::vector<Record>& recs) {
std::ofstream out(path, std::ios::binary);
if (!out) throw std::runtime_error("write failed");
out.write(reinterpret_cast<const char*>(recs.data()),
static_cast<std::streamsize>(recs.size() * sizeof(Record)));
}
std::vector<Record> read_records(const char* path) {
std::ifstream in(path, std::ios::binary | std::ios::ate);
if (!in) throw std::runtime_error("read failed");
auto size = in.tellg(); // file size because ios::ate
in.seekg(0, std::ios::beg);
std::vector<Record> recs(static_cast<std::size_t>(size) / sizeof(Record));
in.read(reinterpret_cast<char*>(recs.data()), size);
if (!in) throw std::runtime_error("short read");
return recs;
}In-place editing with fstream
#include <fstream>
#include <cstdint>
void patch_field(const char* path, std::streamoff offset, std::uint32_t value) {
std::fstream fs(path, std::ios::in | std::ios::out | std::ios::binary);
if (!fs) throw std::runtime_error("cannot open for r/w");
fs.seekp(offset);
fs.write(reinterpret_cast<const char*>(&value), sizeof(value));
}Custom operator<< / operator>> for portable serialisation
Defining operators on std::ostream/std::istream (not on the file stream classes directly) means the same operators work with fstream, stringstream, and cout:
#include <fstream>
#include <string>
#include <stdexcept>
struct Config { std::string host; int port; bool tls; };
std::ostream& operator<<(std::ostream& os, const Config& c) {
return os << c.host << '\n' << c.port << '\n' << c.tls << '\n';
}
std::istream& operator>>(std::istream& is, Config& c) {
return is >> c.host >> c.port >> c.tls;
}
void save(const Config& c, const char* path) {
std::ofstream out(path);
if (!out) throw std::runtime_error("save failed");
out << c;
}
Config load(const char* path) {
std::ifstream in(path);
if (!in) throw std::runtime_error("load failed");
Config c;
if (!(in >> c)) throw std::runtime_error("parse error");
return c;
}Best Practices
Check stream state with if (!stream), not .good() or .is_open() separately.
The implicit bool conversion is equivalent to !fail() && !bad(). A failed open sets failbit, so if (!in) catches missing files. Calling .is_open() as an additional guard is redundant.
Use RAII scoping to guarantee flush-before-read ordering.
Wrap writes in a nested block so the destructor closes and flushes before a reader opens the same file. An explicit .close() call is the alternative when scoping is awkward; the destructor alone is always sufficient.
Prefer binary mode for structured data.
On Windows, text mode silently translates \n to \r\n on write and \r\n to \n on read. For binary formats or network-exchanged files, always pass std::ios::binary.
Use ios::ate + seekg(0) to query file size.
Opening with ate positions the get pointer at end-of-file; tellg() then returns the byte count without a separate stat(2) call.
Accept std::filesystem::path in C++17.
Constructors and open() overloads that accept std::filesystem::path were added in C++17. This is the correct type when composing paths programmatically β it handles directory separators and encoding portably across platforms.
Common Pitfalls
while (!stream.eof()) is always wrong.
eofbit is set after a read fails because end-of-file was reached, so the loop body executes one extra iteration with stale data. Use while (stream >> value) or while (std::getline(stream, line)) β both return a reference to the stream, which evaluates to false on any failure.
std::fstream does not create files that do not exist.
std::fstream fs("new.dat", ios::in | ios::out) fails if new.dat is absent. To create-then-open, add ios::trunc, or first create it via std::ofstream.
reinterpret_cast in binary I/O is only valid for trivially-copyable types.
read/write are correct only when the target type has no padding that matters, no virtual dispatch, and no pointer members. Structs that violate these properties require explicit field-by-field serialisation.
A seek is required when switching between reading and writing on std::fstream.
The standard requires a call to a positioning function (seekg, seekp, flush) between an output operation and a subsequent input operation on the same stream, and vice versa. Omitting it is undefined behaviour.
Streams are not copyable; move semantics require C++11.
Attempting to copy a stream fails to compile. Factory functions that return a stream must rely on move semantics (C++11) or NRVO.
Custom Buffer Size with pubsetbuf
Every file stream owns a std::basic_filebuf accessible via rdbuf(). Calling pubsetbuf(buf, size) on that buffer before the file is opened instructs the implementation to use your memory region as the I/O buffer, reducing the number of read(2)/write(2) syscalls for sequential access. Larger buffers β 64 KB to 1 MB β are most effective when writing many small records.
// C++98 β must call pubsetbuf BEFORE open()
#include <fstream>
#include <array>
#include <cstdio>
int main() {
std::array<char, 1 << 20> buf{}; // 1 MB stack buffer
std::ofstream out;
out.rdbuf()->pubsetbuf(buf.data(), static_cast<std::streamsize>(buf.size()));
out.open("output.bin", std::ios::binary);
if (!out) return 1;
for (int i = 0; i < 1'000'000; ++i)
out.write(reinterpret_cast<const char*>(&i), sizeof(i));
// destructor flushes the large buffer in far fewer syscalls
}Call pubsetbuf before open(); calling it after open is implementation-defined.
On Linux and macOS, a 1 MB buffer typically cuts syscall count by a factor of 256 relative to the 4 KB default. MSVC ignores pubsetbuf for write streams on Windows β use std::setvbuf on a FILE* obtained from _fileno + _fdopen if you need the same guarantee there.
The C-level equivalent for FILE*-based code is std::setvbuf(fp, buf, _IOFBF, size), which must also be called before the first I/O operation.
std::syncstream for Concurrent Writes (C++20)
std::osyncstream is a synchronised wrapper over any std::ostream. Each osyncstream instance buffers its output internally and atomically transfers it to the underlying stream either when emit() is called explicitly or when the osyncstream destructor runs. This prevents interleaving of log lines from multiple threads without requiring an explicit mutex.
// C++20
#include <fstream>
#include <syncstream>
#include <thread>
#include <vector>
void log_worker(std::ostream& sink, int id) {
for (int i = 0; i < 5; ++i) {
std::osyncstream(sink)
<< "thread " << id << " iteration " << i << '\n';
// osyncstream destructor calls emit() here β output is atomic
}
}
int main() {
std::ofstream logfile("app.log");
if (!logfile) return 1;
std::vector<std::thread> threads;
for (int t = 0; t < 4; ++t)
threads.emplace_back(log_worker, std::ref(logfile), t);
for (auto& th : threads) th.join();
}osyncstream does not protect the underlying ofstream from concurrent open()/close() calls β only write operations are synchronised.
Calling emit() explicitly is useful when you want the output to appear before the osyncstream object goes out of scope, for example when logging inside a long-running loop iteration.
Temporary File RAII Guard
Creating a temporary file safely requires generating a unique path, opening it, performing I/O, and reliably deleting it on exit β including on exception. A small RAII class handles all four concerns with std::filesystem (C++17).
// C++17
#include <fstream>
#include <filesystem>
#include <stdexcept>
#include <string>
class TempFile {
public:
explicit TempFile(std::string_view suffix = ".tmp") {
namespace fs = std::filesystem;
path_ = fs::temp_directory_path()
/ (std::string("tmp_") + std::to_string(
std::chrono::steady_clock::now().time_since_epoch().count())
+ std::string(suffix));
stream_.open(path_, std::ios::in | std::ios::out
| std::ios::trunc | std::ios::binary);
if (!stream_)
throw std::runtime_error("cannot create temp file: " + path_.string());
}
~TempFile() {
stream_.close();
std::error_code ec;
std::filesystem::remove(path_, ec); // swallow errors in destructor
}
std::fstream& stream() { return stream_; }
const std::filesystem::path& path() const { return path_; }
TempFile(const TempFile&) = delete;
TempFile& operator=(const TempFile&) = delete;
private:
std::filesystem::path path_;
std::fstream stream_;
};
void process(std::fstream& tmp) {
tmp << "hello temp world\n";
tmp.seekg(0);
std::string line;
std::getline(tmp, line);
}
int main() {
TempFile tf(".dat");
process(tf.stream());
// tf destructor removes the file even if process() throws
}The destructor swallows the std::error_code from remove() to remain noexcept.
C++26 is expected to add std::tmpfile() integration with std::fstream via a new constructor overload, but the RAII pattern above is portable today on all C++17-capable compilers.
std::from_chars for Fast Field Parsing (C++17)
std::from_chars (in <charconv>) converts a character sequence to a number without consulting the global locale, making it 5β10Γ faster than operator>> for numeric fields. The typical workflow combines std::getline for line-oriented input with std::string_view slicing and from_chars for the actual conversion.
// C++17
#include <fstream>
#include <charconv>
#include <string>
#include <string_view>
#include <vector>
#include <stdexcept>
struct Row { int id; double value; };
std::vector<Row> parse_csv(const char* path) {
std::ifstream in(path);
if (!in) throw std::runtime_error("cannot open CSV");
std::vector<Row> rows;
std::string line;
while (std::getline(in, line)) {
std::string_view sv(line);
auto comma = sv.find(',');
if (comma == std::string_view::npos) continue;
Row r{};
// Parse integer id β locale-free, no allocation
auto [ptr1, ec1] = std::from_chars(sv.data(), sv.data() + comma, r.id);
if (ec1 != std::errc{}) continue;
// Parse double value (C++17; all compilers support this in C++23)
std::string_view rest = sv.substr(comma + 1);
auto [ptr2, ec2] = std::from_chars(rest.data(),
rest.data() + rest.size(),
r.value);
if (ec2 != std::errc{}) continue;
rows.push_back(r);
}
return rows;
}from_chars is locale-free by design: it always uses '.' as the decimal separator.
This is the correct choice for data interchange formats (CSV, TSV, binary-text hybrids). C++23 mandates floating-point from_chars support on all conforming implementations β prior to C++23, GCC and MSVC supported it but libstdc++ on some platforms did not.
Seeking and Random Access
std::fstream (and std::ifstream/std::ofstream) expose two independent position indicators: the get pointer, manipulated by seekg/tellg, and the put pointer, manipulated by seekp/tellp. For std::fstream opened with both ios::in and ios::out, the standard requires a positioning call between any output operation and the subsequent input operation, and vice versa.
// C++98
#include <fstream>
#include <cstddef>
#include <stdexcept>
// Query file size without opening in ios::ate mode
std::uintmax_t file_size_via_seek(const char* path) {
std::ifstream in(path, std::ios::binary);
if (!in) throw std::runtime_error("cannot open");
in.seekg(0, std::ios::end);
auto size = in.tellg(); // returns std::streampos
in.seekg(0, std::ios::beg); // rewind for subsequent reads
return static_cast<std::uintmax_t>(size);
}
struct Header { std::uint32_t magic; std::uint32_t count; };
struct Record { std::uint32_t id; double value; };
// Jump directly to record N in a fixed-layout binary file
Record read_record(std::ifstream& in, std::size_t n) {
std::streamoff offset = static_cast<std::streamoff>(sizeof(Header))
+ static_cast<std::streamoff>(n * sizeof(Record));
in.seekg(offset, std::ios::beg);
Record r{};
in.read(reinterpret_cast<char*>(&r), sizeof(r));
if (!in) throw std::runtime_error("record read failed");
return r;
}
// Switch from write to read on the same fstream
void write_then_read(std::fstream& fs) {
fs << "hello";
fs.seekp(0, std::ios::cur); // no-op seek satisfies the standard requirement
std::string s;
fs >> s;
}seekp(0, ios::cur) is the idiomatic no-op seek required when switching from output to input on std::fstream.
std::streamoff arithmetic is safe for structured binary files: multiplying record count by sizeof(Record) gives an exact byte offset as long as the struct is trivially copyable and has no intra-record padding that varies across platforms. std::streampos is not an integer type β always cast through std::streamoff for arithmetic.
Streaming to std::print / std::println (C++23)
std::print and std::println (in <print>) use std::format internally and write to a FILE*. By default they target stdout, but an overload accepting FILE* as the first argument lets you direct output to any C-level file handle.
// C++23
#include <print>
#include <cstdio>
int main() {
std::FILE* fp = std::fopen("output.txt", "w");
if (!fp) return 1;
std::println(fp, "value={}, pi={:.4f}", 42, 3.14159);
std::fclose(fp);
// Quick stdout example
int x = 7;
std::println(std::cout, "value={}", x); // C++23: ostream overload
}Bridging std::print to std::ofstream requires converting the stream's underlying file descriptor to a FILE*. On POSIX this is achievable via fileno() + fdopen(), but the result is a separate FILE* with its own buffer β the two buffers can interleave if both are written without explicit flushes.
// C++23 β POSIX only; avoid in portable code
#include <fstream>
#include <print>
#include <cstdio>
#ifdef __unix__
#include <ext/stdio_filebuf.h> // GCC extension
void mixed_write(const char* path) {
std::ofstream out(path);
if (!out) return;
out << "line via ofstream\n";
out.flush();
// Obtain FILE* from the underlying fd β POSIX only
int fd = static_cast<__gnu_cxx::stdio_filebuf<char>*>(out.rdbuf())->fd();
std::FILE* fp = ::fdopen(fd, "a");
if (fp) {
std::print(fp, "line via std::print fd={}\n", fd);
std::fflush(fp);
// Do NOT fclose(fp) β ofstream still owns the fd
}
}
#endifThe portable pattern is std::format for string construction combined with ofstream << for output.
This avoids any FILE* / std::fstream impedance mismatch entirely and works on all C++23-capable compilers including MSVC:
// C++23 β portable
#include <fstream>
#include <format>
int main() {
std::ofstream out("log.txt");
if (!out) return 1;
out << std::format("value={}, pi={:.4f}\n", 42, 3.14159);
}See Also
<sstream>β in-memory string streams sharing the same interface as file streams<charconv>β locale-independent number-to-string conversion, useful when parsing individual fields read from a text file<filesystem>βstd::filesystem::pathfor constructing and introspecting file paths passed to stream constructors (C++17)