Filesystem Library
The Filesystem Library
Section titled “The Filesystem Library”std::filesystem (C++17) provides a portable interface for manipulating paths, querying file Metadata, iterating directories, and performing file operations. It abstracts away platform Differences between POSIX and Windows file systems, normalizing path separators, permissions models, And file metadata into a common interface. This section covers std::filesystem::pathDirectory Iterators, recursive traversal, and common file operations.
Overview
Section titled “Overview”std::filesystem (C++17) provides a portable interface for manipulating paths, querying file Metadata, iterating directories, and performing file operations [N4950 §30.10]. It is declared in <filesystem> and lives in the std::filesystem namespace.
The library abstracts away platform differences between POSIX and Windows file systems. Path Separators (/ vs \), permissions models, and file metadata are normalized into a common Interface.
std::filesystem::path is a portable path type that stores a sequence of path components and Provides methods for manipulation, decomposition, and composition [N4950 §30.10.7].
#include <filesystem>#include <iostream>
namespace fs = std::filesystem;
void path_basics() { fs::path p = "/usr/local/bin/cpp";
std::cout << "Path: " << p << "\n"; std::cout << "Root name: " << p.root_name() << "\n"; // "" (no drive on POSIX) std::cout << "Root directory: " << p.root_directory() << "\n"; // "/" std::cout << "Root path: " << p.root_path() << "\n"; // "/" std::cout << "Relative: " << p.relative_path() << "\n"; // "usr/local/bin/cpp" std::cout << "Parent: " << p.parent_path() << "\n"; // "/usr/local/bin" std::cout << "Filename: " << p.filename() << "\n"; // "cpp" std::cout << "Stem: " << p.stem() << "\n"; // "cpp" std::cout << "Extension: " << p.extension() << "\n"; // ""
fs::path p2 = "archive.tar.gz"; std::cout << "Stem: " << p2.stem() << "\n"; // "archive.tar" std::cout << "Extension: " << p2.extension() << "\n"; // ".gz"}Path Composition
Section titled “Path Composition”fs::path supports / as an operator for path concatenation, which automatically inserts the Correct separator:
#include <filesystem>#include <iostream>
namespace fs = std::filesystem;
void path_composition() { fs::path base = "/home/user"; fs::path full = base / "projects" / "my_app" / "src" / "main.cpp"; std::cout << full << "\n"; // /home/user/projects/my_app/src/main.cpp
// On Windows, this correctly produces: // C:\home\user\projects\my_app\src\main.cpp // (even if base is C:/home/user — the / operator normalizes separators)
fs::path canonical = fs::canonical(full); // Resolves symlinks, removes . and .., produces absolute path}Path Iteration
Section titled “Path Iteration”The begin() / end() methods on fs::path iterate over the path components:
#include <filesystem>#include <iostream>
namespace fs = std::filesystem;
void path_iteration() { fs::path p = "/usr/local/bin/../lib/libstdc++.so.6";
for (const auto& component : p) { std::cout << "[" << component.string() << "] "; } // [/] [usr] [local] [bin] [..] [lib] [libstdc++.so.6]}The library provides two directory iterators [N4950 §30.10.11]:
| Iterator | Behavior |
|---|---|
fs::directory_iterator | Iterates over the immediate children of a directory |
fs::recursive_directory_iterator | Iterates over all descendants recursively |
Both are input iterators and follow the RAII pattern — they close the directory handle in their Destructor.
#include <filesystem>#include <iostream>#include <string>
namespace fs = std::filesystem;
void list_directory(const fs::path& dir) { if (!fs::exists(dir) || !fs::is_directory(dir)) { std::cerr << "Not a directory: " << dir << "\n"; return; }
for (const auto& entry : fs::directory_iterator(dir)) { const auto& path = entry.path(); std::string type = entry.is_directory() ? "[DIR] " : "[FILE]"; std::uintmax_t size = entry.is_regular_file() ? entry.file_size() : 0; std::cout << type << " " << path.filename().string() << " (" << size << " bytes)\n"; }}#include <filesystem>#include <iomanip>#include <iostream>#include <string>
namespace fs = std::filesystem;
void recursive_list(const fs::path& root, int max_depth = 3) { if (!fs::exists(root) || !fs::is_directory(root)) { std::cerr << "Not a directory: " << root << "\n"; return; }
std::uintmax_t total_files = 0; std::uintmax_t total_dirs = 0; std::uintmax_t total_bytes = 0;
for (const auto& entry : fs::recursive_directory_iterator( root, fs::directory_options::skip_permission_denied)) {
int depth = static_cast<int>(entry.depth());
if (depth > max_depth) { // Prevent descent beyond max_depth by calling disable_recursion_pending entry.disable_recursion_pending(); continue; }
const auto& path = entry.path(); std::string indent(depth * 2, " ');
if (entry.is_directory()) { ++total_dirs; std::cout << indent << "[DIR] " << path.filename().string() << "/\n"; } else if (entry.is_regular_file()) { ++total_files; std::uintmax_t size = entry.file_size(); total_bytes += size; std::cout << indent << "[FILE] " << path.filename().string() << " (" << size << " bytes)\n"; } else if (entry.is_symlink()) { std::cout << indent << "[LINK] " << path.filename().string() << " -> " << fs::read_symlink(path).string() << "\n"; } }
std::cout << "\nSummary: " << total_dirs << " directories, " << total_files << " files, " << total_bytes << " bytes\n";}The std::filesystem namespace provides free functions for common file operations [N4950 §30.10.14]:
| Function | Purpose |
|---|---|
fs::copy(src, dst, options) | Copy files or directories |
fs::copy_file(src, dst, options) | Copy a single file (overwrite control) |
fs::remove(path) | Remove a file or empty directory |
fs::remove_all(path) | Remove a file or directory tree recursively |
fs::rename(old, new) | Rename or move a file/directory |
fs::resize_file(path, size) | Truncate or extend a file |
fs::space(path) | Get disk space info (capacity, free, available) |
fs::status(path) | Get file status (type, permissions) |
fs::last_write_time(path) | Get/set last modification time |
fs::create_directory(path) | Create a single directory |
fs::create_directories(path) | Create directory tree (like mkdir -p) |
fs::current_path() | Get/set the current working directory |
fs::temp_directory_path() | Get the system temp directory |
#include <filesystem>#include <fstream>#include <iostream>
namespace fs = std::filesystem;
void file_operations_demo(const fs::path& work_dir) { fs::create_directories(work_dir / "src"); fs::create_directories(work_dir / "build");
fs::path source = work_dir / "src" / "main.cpp"; { std::ofstream f(source); f << "#include <iostream>\nint main() { return 0; }\n"; }
fs::copy_file(source, work_dir / "build" / "main.cpp.bak", fs::copy_options::overwrite_existing);
fs::path moved = work_dir / "build" / "backup.cpp"; fs::rename(work_dir / "build" / "main.cpp.bak", moved);
fs::space_info si = fs::space(work_dir); std::cout << "Capacity: " << si.capacity << " bytes\n"; std::cout << "Free: " << si.free << " bytes\n"; std::cout << "Available: " << si.available << " bytes\n";
fs::file_status st = fs::status(source); std::cout << "Type: " << (fs::is_regular_file(st) ? "regular file" : "other") << "\n";
auto perms = st.permissions(); std::cout << "Readable: " << ((perms & fs::perms::owner_read) != fs::perms::none) << "\n"; std::cout << "Writable: " << ((perms & fs::perms::owner_write) != fs::perms::none) << "\n"; std::cout << "Executable:" << ((perms & fs::perms::owner_exec) != fs::perms::none) << "\n";
// Cleanup fs::remove_all(work_dir); std::cout << "Removed: " << work_dir << "\n";}File Metadata and fs::file_time_type
Section titled “File Metadata and fs::file_time_type”fs::file_time_type represents file timestamps [N4950 §30.10.7.3]. In C++17/20, this is a std::chrono::time_point using a filesystem-specific clock. In C++20, conversions between file_time_type and std::chrono::system_clock::time_point are possible via clock_cast:
#include <chrono>#include <filesystem>#include <iostream>
namespace fs = std::filesystem;
void file_time_demo(const fs::path& file) { auto ftime = fs::last_write_time(file);
// C++20: convert to system_clock time_point auto sctp = std::chrono::clock_cast<std::chrono::system_clock>(ftime);
// Format the time std::time_t tt = std::chrono::system_clock::to_time_t(sctp); std::tm* tm = std::localtime(&tt); char buf[64]; std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm); std::cout << "Last modified: " << buf << "\n";
// Set modification time (copy from another file) // fs::last_write_time(target, fs::last_write_time(source));}File permissions on std::filesystem are modeled as a bitmask of fs::perms enumerators [N4950 §30.10.7.4]. These map directly to POSIX permission bits on Linux and are partially supported on Windows:
#include <filesystem>#include <iostream>
namespace fs = std::filesystem;
void permissions_demo(const fs::path& file) { // Query permissions auto perms = fs::status(file).permissions();
auto has = [&](fs::perms p) { return (perms & p) != fs::perms::none; };
std::cout << "Owner read: " << has(fs::perms::owner_read) << "\n"; std::cout << "Owner write: " << has(fs::perms::owner_write) << "\n"; std::cout << "Owner exec: " << has(fs::perms::owner_exec) << "\n"; std::cout << "Group read: " << has(fs::perms::group_read) << "\n"; std::cout << "Others read: " << has(fs::perms::others_read) << "\n";
// Set permissions (POSIX-style octal: 0644) fs::permissions(file, fs::perms::owner_read | fs::perms::owner_write | fs::perms::group_read | fs::perms::others_read, fs::perm_options::replace);
// Add execute permission for owner fs::permissions(file, fs::perms::owner_exec, fs::perm_options::add);}fs::perm_options | Effect |
|---|---|
replace | Replace current permissions with the given bitmask |
add | Add the given permission bits to the current set |
remove | Remove the given permission bits from the current set |
nofollow | Do not follow symlinks (applicable on POSIX) |
std::filesystem distinguishes between the symlink itself and its target [N4950 §30.10.10]:
#include <filesystem>#include <iostream>
namespace fs = std::filesystem;
void symlink_demo(const fs::path& dir) { fs::path target = dir / "original.txt"; fs::path link = dir / "link_to_original";
// Create a file and a symlink to it { std::ofstream f(target); f << "original content"; }
fs::create_symlink(target, link);
// status() follows symlinks, symlink_status() does not std::cout << "status(target): " << fs::status(target) << "\n"; std::cout << "status(link): " << fs::status(link) << "\n"; // regular file std::cout << "symlink_status(link): " << fs::symlink_status(link) << "\n"; // symlink
std::cout << "is_symlink(link): " << fs::is_symlink(link) << "\n"; // true std::cout << "is_regular_file(link): " << fs::is_regular_file(link) << "\n"; // true (follows)
// Read the symlink target fs::path resolved = fs::read_symlink(link); std::cout << "Symlink target: " << resolved << "\n";
// Resolve the full path (follows symlinks) fs::path canonical_path = fs::canonical(link); std::cout << "Canonical: " << canonical_path << "\n";
// Cleanup fs::remove(link); fs::remove(target);}File Size and Disk Space
Section titled “File Size and Disk Space”#include <filesystem>#include <iostream>#include <vector>
namespace fs = std::filesystem;
void disk_space_demo(const fs::path& dir) { fs::space_info space = fs::space(dir);
auto to_gb = [](std::uintmax_t bytes) { return static_cast<double>(bytes) / (1024.0 * 1024.0 * 1024.0); };
std::cout << "Capacity: " << to_gb(space.capacity) << " GB\n"; std::cout << "Free: " << to_gb(space.free) << " GB\n"; std::cout << "Available: " << to_gb(space.available) << " GB\n";
// "Free" may include space reserved for root (on POSIX). // "Available" is the space available to unprivileged processes. // On most systems: available <= free <= capacity.}
void directory_size(const fs::path& root) { std::uintmax_t total_size = 0; std::uintmax_t file_count = 0;
for (const auto& entry : fs::recursive_directory_iterator( root, fs::directory_options::skip_permission_denied)) { if (entry.is_regular_file()) { total_size += entry.file_size(); ++file_count; } }
std::cout << "Total size: " << total_size << " bytes (" << file_count << " files)\n";}Path Canonicalization and weakly_canonical
Section titled “Path Canonicalization and weakly_canonical”fs::canonical resolves symlinks and normalizes the path, but requires that every component exists. fs::weakly_canonical [N4950 §30.10.7.7] resolves as much as it can, leaving non-existent Components untouched:
#include <filesystem>#include <iostream>
namespace fs = std::filesystem;
void canonical_demo() { // canonical() throws if any component doesn't exist // fs::canonical("/nonexistent/path/file.txt"); // throws
// weakly_canonical() resolves existing components, leaves the rest fs::path wc = fs::weakly_canonical("/home/user/nonexistent/dir/file.txt"); std::cout << "Weakly canonical: " << wc << "\n"; // /home/user/nonexistent/dir/file.txt // (assuming /home/user exists but /home/user/nonexistent does not)
// lexically_normal() is purely string-based — no filesystem access fs::path normalized = fs::path("/a/b/../c/./d").lexically_normal(); std::cout << "Lexically normal: " << normalized << "\n"; // /a/c/d}Filesystem Error Handling
Section titled “Filesystem Error Handling”The throwing overloads of fs:: functions throw fs::filesystem_error on failure [N4950 §30.10.5]. Non-throwing overloads accept an std::error_code& parameter. filesystem_error carries two paths (the source and target of the operation) and a std::error_code:
#include <filesystem>#include <iostream>
namespace fs = std::filesystem;
void error_handling_demo() { fs::path nonexistent = "/this/path/does/not/exist";
try { fs::copy(nonexistent, "/tmp/backup"); } catch (const fs::filesystem_error& e) { std::cout << "Error: " << e.what() << "\n"; std::cout << "Path 1: " << e.path1() << "\n"; std::cout << "Path 2: " << e.path2() << "\n"; std::cout << "Error code: " << e.code() << "\n"; // Error code: system:2 (No such file or directory) }
// Non-throwing overload — returns error code std::error_code ec; fs::file_size(nonexistent, ec); if (ec) { std::cout << "Error (non-throwing): " << ec.message() << "\n"; }}A common pattern for safe file writing is to write to a temporary file, then atomically rename it:
#include <filesystem>#include <fstream>#include <string>
namespace fs = std::filesystem;
void atomic_write(const fs::path& target, std::string_view content) { fs::path temp = target; temp += ".tmp";
// Write to temporary file { std::ofstream f(temp, std::ios::binary); if (!f) { throw std::runtime_error("Cannot create temp file: " + temp.string()); } f.write(content.data(), static_cast<std::streamsize>(content.size())); }
// Atomic rename (on POSIX, rename is atomic for same-filesystem renames) fs::rename(temp, target);
// On POSIX: rename() replaces the target atomically. // On Windows: rename() fails if target exists. Use MoveFileEx with MOVEFILE_REPLACE_EXISTING. // std::filesystem::rename handles this difference, but the atomicity guarantee // may differ across platforms.}Common Pitfalls
Section titled “Common Pitfalls”fs::pathcomparison is lexicographic:fs::path("a/b") != fs::path("a//b"). They are not the same path object even though they refer to the same file. Usefs::canonical()orfs::equivalent()for semantic comparison.fs::remove_allis not undoable: There is no “trash” mechanism. Once called, the files are gone. Never call it with untrusted input without validation.Symlink cycles:
fs::recursive_directory_iteratorcan follow symlink cycles indefinitely if you usefollow_directory_symlink. Usedirectory_options::follow_directory_symlinkwith caution, and consider tracking visited directories by device/inode to detect cycles.Path separator on Windows:
fs::pathuses the preferred separator on construction, but the/operator always works on both platforms. Avoid hardcoding\\— always use/orfs::path::preferred_separator.fs::existsTOCTOU race: The checkif (fs::exists(p)) fs::remove(p)is vulnerable to a time-of-check-to-time-of-use (TOCTOU) race — another process may create or delete the file between the check and the removal. Prefer just performing the operation and handling the error.File permissions on Windows: The POSIX permission model does not map cleanly to Windows ACLs.
fs::permissionson Windows can only reliably set/clear the read-only attribute. Do not rely on group/other permissions on Windows.
Common Pitfalls
Section titled “Common Pitfalls”Memorising content without understanding the underlying principles. This leads to poor application in unfamiliar contexts.
Ignoring feedback from marked work and failing to address recurring weaknesses.
Focusing only on content knowledge without developing exam technique and question-answering skills.
Not practising with past papers or exercises under timed conditions.
Intuition
Section titled “Intuition”The filesystem library is like a universal remote for files: Instead of using platform-specific APIs (open on Linux, CreateFile on Windows), std::filesystem provides a single interface that works everywhere. It’s like having a remote control that works with any TV — you don’t need to know the brand, just point and press. The library handles path normalization, directory traversal, file attributes, and error handling across platforms.
Why it matters: The filesystem library eliminates platform-specific code for file operations. Instead of writing #ifdef _WIN32 blocks, you write std::filesystem::exists(path) and it works everywhere. It also provides safe error handling with std::error_code instead of exceptions, and supports operations like recursive directory traversal, path manipulation, and file metadata queries.
The key insight: std::filesystem provides platform-independent file operations — write once, run on any OS.
Summary
Section titled “Summary”The key principles covered in this topic are linked in the sub-pages above. Focus on understanding the definitions, applying the formulas or frameworks, and evaluating strengths and limitations of each approach.
Worked Examples
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.