C++ Filesystem


  • Description: A note on std::filesystem (C++17) — path, queries, traversal, file operations, error handling, and cross-platform considerations
  • My Notion Note ID: K2A-B1-23
  • Created: 2018-07-10
  • Updated: 2026-02-28
  • License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io

Table of Contents


1. std::filesystem::path

std::filesystem::path (C++17, <filesystem>) is the type for filesystem locations. It handles encoding, separators, and decomposition uniformly across platforms.

#include <filesystem>
namespace fs = std::filesystem;

fs::path p = "/var/log/app.log";

p.string();              // "/var/log/app.log"
p.filename();            // "app.log"
p.stem();                // "app"
p.extension();           // ".log"
p.parent_path();         // "/var/log"

p.replace_extension(".bak");   // "/var/log/app.bak"

// Composition with /=  and  /
fs::path dir = "/var/log";
fs::path file = dir / "app.log";        // "/var/log/app.log"
file /= "rotated";                       // "/var/log/app.log/rotated"
                                         // /= treats both sides as directory parts

Path arithmetic (/, /=) handles the platform separator automatically. Don't manually concatenate strings.


2. Queries: Existence, Type, Size, Time

namespace fs = std::filesystem;

fs::path p = "/etc/hosts";

fs::exists(p);                  // true
fs::is_regular_file(p);         // true
fs::is_directory(p);            // false
fs::is_symlink(p);              // false
fs::is_empty(p);                // false (file is non-empty)

fs::file_size(p);               // size in bytes (regular files only)

auto t = fs::last_write_time(p);          // file_clock time_point
auto s = fs::status(p);                    // permissions + type (cached)
auto perms = s.permissions();              // permission bits

status(p) is the cheapest single-stat query; the boolean helpers like is_directory(p) re-stat the path each call. For tight loops, cache:

auto s = fs::status(p);
if (fs::is_regular_file(s)) { /* ... */ }   // reuses the cached status

3. Iterating Directories

namespace fs = std::filesystem;

// Non-recursive
for (const auto& entry : fs::directory_iterator{"/var/log"}) {
    std::cout << entry.path() << "\n";
}

// Recursive
for (const auto& entry : fs::recursive_directory_iterator{"/var/log"}) {
    if (entry.is_regular_file()) {
        std::cout << entry.path() << " (" << entry.file_size() << ")\n";
    }
}

Directory iterators return directory_entry objects, which cache file status for cheap repeated queries.


4. File Operations

namespace fs = std::filesystem;

// Create
fs::create_directory("/tmp/foo");
fs::create_directories("/tmp/foo/bar/baz");     // mkdir -p

// Copy / move
fs::copy("a.txt", "b.txt");
fs::copy("src/", "dst/", fs::copy_options::recursive | fs::copy_options::overwrite_existing);
fs::rename("old.txt", "new.txt");

// Remove
fs::remove("file.txt");                          // single file
fs::remove_all("/tmp/foo");                      // recursive (returns count)

// Symlinks
fs::create_symlink("/var/log/app.log", "current.log");
fs::read_symlink("current.log");                 // "/var/log/app.log"

// Permissions
fs::permissions("script.sh", fs::perms::owner_exec, fs::perm_options::add);

5. Error Handling

Every filesystem function has two overloads:

  1. Throws std::filesystem::filesystem_error on failure.
  2. Takes a std::error_code& and reports failure through it (no throw).
namespace fs = std::filesystem;

// Throwing version
try {
    auto sz = fs::file_size("missing.txt");
} catch (const fs::filesystem_error& e) {
    std::cerr << e.what() << " path: " << e.path1() << "\n";
}

// Non-throwing version
std::error_code ec;
auto sz = fs::file_size("missing.txt", ec);
if (ec) {
    std::cerr << "error: " << ec.message() << "\n";
}

Pick whichever matches your existing error-handling style. See C++ Error Handling § 6.


6. Cross-Platform Considerations

  1. Encoding. On POSIX, paths are char (typically UTF-8). On Windows, they are wchar_t (UTF-16). path::native() returns the platform-native representation; path::string() converts (lossy on Windows for non-Latin filenames, use path::u8string() for UTF-8).
  2. Separators. Use / in literals; path translates as needed. Avoid hardcoding backslashes on Windows.
  3. Case sensitivity. POSIX is case-sensitive; Windows and macOS HFS+/APFS default to case-insensitive but case-preserving. Don't rely on case-only differences.
  4. Permissions. POSIX perms map cleanly; Windows ACLs do not, so the permissions() API is approximate on Windows.
  5. Symbolic links. Windows requires admin or developer mode to create symlinks; the call may fail otherwise.