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.
#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
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/query | Meaning |
|---|---|
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
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.
#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.
#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
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.
| Decision | State explicitly |
|---|---|
| Text encoding | For example UTF-8 |
| Integer representation | Width, signedness, and byte order |
| Lengths | Maximum accepted size and unit |
| Version | Compatibility and migration policy |
| Unknown fields | Reject, ignore, or preserve |
| Integrity | Checksums/authentication where the threat model needs them |
Key idea
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.
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
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++.