Phase 3 · Generic Programming & the Standard LibraryModule 19~54 min read

Streams, Filesystem & Serialization

Process files and directories, parse structured text, and design explicit versioned formats for persistent data.

What you'll learn

Files are external resources with partial reads, permissions, platform-specific paths, and untrusted bytes. You will use RAII streams and filesystem paths, parse structured text, and design explicit versioned formats instead of dumping in-memory objects.

By the end, you'll be able to:

  • Read and write text or binary files with explicit stream checks
  • Parse structured records without accepting partial input
  • Traverse and manipulate paths with std::filesystem
  • Design versioned, validated, portable serialization and reliable replacement

RAII file streams

std::ifstream reads files, std::ofstream writes them, andstd::fstream supports both directions. Construction opens the file and destruction closes it. Always check that opening succeeded before relying on later operations.

copy_text.cpp
#include <fstream>
#include <stdexcept>
#include <string>

int main() {
    std::ifstream input{"source.txt"};
    if (!input) throw std::runtime_error{"cannot open source.txt"};

    std::ofstream output{"copy.txt"};
    if (!output) throw std::runtime_error{"cannot create copy.txt"};

    for (std::string line; std::getline(input, line); ) {
        output << line << '\n';
    }
    if (!input.eof()) throw std::runtime_error{"read failed"};
    if (!output) throw std::runtime_error{"write failed"};
}

Key idea

A successful open does not guarantee every later read, write, flush, or close succeeds. Check at the boundary where your application must promise durable output.

State, buffering, and positioning

Streams track goodbit, eofbit, failbit, andbadbit. Form reads as a condition so processing occurs only after a successful extraction. seekg/tellg operate on the input position; seekp/tellpoperate on the output position.

State/queryMeaning
eof()End-of-file was encountered by an operation
fail()Formatted extraction or another recoverable operation failed
bad()Serious underlying I/O failure
clear()Reset state flags; does not remove input bytes
flush()Ask the stream buffer to forward pending output

Watch out

Do not write while (!input.eof()). EOF is set only after an attempted read reaches the end, so that pattern can process stale data one extra time.

Structured text parsing

Read a whole record boundary, then parse it with std::istringstream. Validate required fields, ranges, delimiters, and trailing junk before committing the result. This keeps malformed external input from partially modifying domain objects.

parse_record.cpp
#include <iostream>
#include <optional>
#include <sstream>
#include <string>

struct Record { int id; std::string name; double score; };

std::optional<Record> parse_record(const std::string& line) {
    std::istringstream input{line};
    Record result{};
    char comma1{};
    char comma2{};
    if (input >> result.id >> comma1 >> result.name >> comma2 >> result.score
        && comma1 == ',' && comma2 == ',' && input >> std::ws && input.eof()) {
        return result;
    }
    return std::nullopt;
}

int main() {
    if (auto record{parse_record("7,Maya,94.5")}) {
        std::cout << record->name << ' ' << record->score << '\n';
    }
}

Paths and directories

std::filesystem::path represents native path syntax and supports composition, decomposition, and normalization. Directory iterators enumerate entries. Most operations have a throwing overload and an overload accepting std::error_code; choose one policy and do not accidentally ignore errors.

list_cpp.cpp
#include <filesystem>
#include <iostream>
#include <system_error>

namespace fs = std::filesystem;

int main() {
    std::error_code error;
    for (fs::directory_iterator it{".", error}, end; it != end && !error; it.increment(error)) {
        const fs::directory_entry& entry{*it};
        if (entry.is_regular_file(error) && entry.path().extension() == ".cpp") {
            std::cout << entry.path().filename().string() << '\n';
        }
    }
    if (error) std::cerr << "filesystem error: " << error.message() << '\n';
}

Watch out

Do not build paths by concatenating slash-containing strings. Use path / component, and never accept untrusted relative components without checking traversal and allowed roots.

Portable serialization

Serialization defines an external format; object memory layout is not that format. Padding, endianness, integer widths, pointers, compiler choices, and class changes make raw memory dumps non-portable and often unsafe. Define field names or numeric tags, sizes, encodings, and version rules explicitly.

DecisionState explicitly
Text encodingFor example UTF-8
Integer representationWidth, signedness, and byte order
LengthsMaximum accepted size and unit
VersionCompatibility and migration policy
Unknown fieldsReject, ignore, or preserve
IntegrityChecksums/authentication where the threat model needs them

Key idea

Treat every deserializer as an untrusted-input parser. Validate lengths before allocation, numeric ranges before conversion, and the entire record before changing application state.

Reliable replacement

Overwriting a valuable file in place risks leaving it truncated if the process fails. Write a complete temporary file in the same target directory, verify and close it, then replace the destination with the platform's appropriate rename strategy. True durability may also require syncing file and directory metadata; those details are platform-specific.

replacement_plan.txt
1. Validate the new in-memory document.
2. Create a uniquely named temporary file beside the target.
3. Serialize every record and check stream state.
4. Flush, close, and perform any required platform durability sync.
5. Atomically replace the destination where the platform supports it.
6. Preserve or report recovery information if replacement fails.

Note

“Rename is atomic” is not a universal durability guarantee across filesystems, devices, or operating systems. Define the level of reliability the application actually needs and test on supported targets.

Recap & quick check

Key takeaways

  • File streams are RAII objects, but every meaningful I/O boundary still needs error checks.
  • Read operations belong in loop conditions; eof() reports a state after an attempted read.
  • Parse complete records into temporary values, validate fully, then commit.
  • filesystem::path handles native composition more safely than slash-concatenated strings.
  • Portable serialization defines encoding, sizes, byte order, versions, limits, and replacement policy.

Quick check

1. Why is while (!input.eof()) incorrect?

2. What should be used to combine path components?

3. Why not dump a class's raw bytes as a file format?

4. What is the safe commit sequence for parsing?

Phase 3 complete. Next, Module 20 — Value Categories, Move Semantics & Perfect Forwarding begins advanced lifetime-sensitive C++.