The std::filesystem Library (C++17)
Before C++17, there was no portable way to work with files and directories. You had to use platform-specific APIs (WinAPI on Windows, POSIX on Linux/macOS) or third-party libraries like Boost. std::filesystem (based on Boost.Filesystem) gives C++ a standard, cross-platform API for file system operations.
Key abstractions: std::filesystem::path — a file or directory path std::filesystem::directory_entry — metadata about one entry std::filesystem::directory_iterator — iterate a directory std::filesystem::recursive_directory_iterator — iterate recursively
The namespace is long, so most code uses an alias: namespace fs = std::filesystem;
#include <iostream>
#include <format>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
namespace fs = std::filesystem;Frequently Asked Questions
QWhat IS a "path"? Is it a string?
QForward slash or backslash?
QDo these operations throw exceptions?
QIs std::filesystem thread-safe?
QWhat's the difference between "path" and "canonical path"?
QHow do I get the current working directory?
HOW fs::path STORES AND MANIPULATES PATHS
Internally, fs::path stores the path in the OS's preferred format: - Windows: std::wstring (wide characters, UTF-16) - POSIX: std::string (narrow characters, typically UTF-8)
The path is decomposed into components:
fs::path p = "/home/user/docs/report.txt";
p.root_name() → "" (or "C:" on Windows) p.root_directory() → "/" p.root_path() → "/" (root_name + root_directory) p.relative_path() → "home/user/docs/report.txt" p.parent_path() → "/home/user/docs" p.filename() → "report.txt" p.stem() → "report" (filename without extension) p.extension() → ".txt" (including the dot)
Path concatenation: p / "subdir" → "/home/user/docs/report.txt/subdir" operator/ is overloaded to join paths with the platform separator.
p.replace_extension(".pdf") → "/home/user/docs/report.pdf" p.replace_filename("notes.md") → "/home/user/docs/notes.md"
1 Path manipulation
Path manipulation.
Use this when it cleanly solves the problem in front of you.
It improves correctness, clarity, and maintainability.
Use it in code like: void demo_paths() {.
void demo_paths() {
std::cout << "=== 1. Path manipulation ===\n\n";
fs::path p = "C:/Users/student/projects/main.cpp";
// On Windows this could also be "C:\\Users\\student\\projects\\main.cpp"
// fs::path normalizes both forms.
std::cout << std::format(" Full path: {}\n", p.string());
std::cout << std::format(" Root name: {}\n", p.root_name().string());
std::cout << std::format(" Root dir: {}\n", p.root_directory().string());
std::cout << std::format(" Parent: {}\n", p.parent_path().string());
std::cout << std::format(" Filename: {}\n", p.filename().string());
std::cout << std::format(" Stem: {}\n", p.stem().string());
std::cout << std::format(" Extension: {}\n", p.extension().string());
// --- Building paths with / operator ---
fs::path base = "C:/Projects";
fs::path full = base / "MyApp" / "src" / "main.cpp";
std::cout << std::format("\n Built path: {}\n", full.string());
// operator/ adds the correct separator for the platform.
// NEVER manually concatenate with + and hardcoded separators.
// --- Modifying paths ---
fs::path doc = "report.txt";
doc.replace_extension(".pdf");
std::cout << std::format(" After replace_extension: {}\n", doc.string());
// --- Checking path properties ---
fs::path abs = "C:/absolute/path";
fs::path rel = "relative/path";
std::cout << std::format("\n \"{}\" is absolute? {}\n",
abs.string(), abs.is_absolute());
std::cout << std::format(" \"{}\" is relative? {}\n",
rel.string(), rel.is_relative());
}2 Checking existence and file types
Checking existence and file types.
Use this when it cleanly solves the problem in front of you.
It improves correctness, clarity, and maintainability.
Use it in code like: void demo_existence_checks() {.
void demo_existence_checks() {
std::cout << "\n=== 2. Existence and type checks ===\n\n";
// Check the current directory
fs::path cwd = fs::current_path();
std::cout << std::format(" Current directory: {}\n", cwd.string());
// Check various properties
std::cout << std::format(" Exists? {}\n", fs::exists(cwd));
std::cout << std::format(" Is directory? {}\n", fs::is_directory(cwd));
std::cout << std::format(" Is file? {}\n", fs::is_regular_file(cwd));
// Check a file that probably doesn't exist (using error_code overload)
fs::path fake = "this_file_does_not_exist_xyz.tmp";
std::error_code ec;
bool exists = fs::exists(fake, ec);
std::cout << std::format(" \"{}\" exists? {} (no exception thrown)\n",
fake.string(), exists);
// The error_code overload is safer when checking — it won't throw
// if the path is invalid or permissions are denied.
}3 Creating and removing directories
Creating and removing directories.
Use this when it cleanly solves the problem in front of you.
It improves correctness, clarity, and maintainability.
Use it in code like: void demo_directory_operations() {.
Watch out: remove_all is DANGEROUS — it's the C++ equivalent of rm -rf. Always double-check the path before calling it. Consider using remove() in a loop with logging for safety.
void demo_directory_operations() {
std::cout << "\n=== 3. Directory operations ===\n\n";
fs::path demo_dir = "demo_filesystem_test";
fs::path nested = demo_dir / "level1" / "level2" / "level3";
// --- Create nested directories ---
// create_directory: creates ONE directory (parent must exist)
// create_directories: creates ALL missing directories in the path
fs::create_directories(nested);
std::cout << std::format(" Created: {}\n", nested.string());
// Create a file inside the nested directory
{
std::ofstream(nested / "test.txt") << "Hello from nested dir!\n";
}
// --- List directory contents ---
std::cout << "\n Contents of demo_filesystem_test (recursive):\n";
for (const auto& entry : fs::recursive_directory_iterator(demo_dir)) {
// entry.path() gives the full path
// entry.is_directory() / entry.is_regular_file() check the type
std::string type = entry.is_directory() ? "[DIR]" : "[FILE]";
// Calculate depth by counting how deep we are relative to demo_dir
auto rel = fs::relative(entry.path(), demo_dir);
std::cout << std::format(" {} {}\n", type, rel.string());
}
// --- Remove everything ---
// remove: deletes ONE file or EMPTY directory
// remove_all: deletes a directory and ALL its contents (like rm -rf)
auto removed = fs::remove_all(demo_dir);
std::cout << std::format("\n Removed {} items\n", removed);
}4 Copying and renaming files
Copying and renaming files.
Use this when it cleanly solves the problem in front of you.
It improves correctness, clarity, and maintainability.
Use it in code like: void demo_copy_rename() {.
void demo_copy_rename() {
std::cout << "\n=== 4. Copy and rename ===\n\n";
// Create a source file
fs::path src = "demo_source.txt";
{ std::ofstream(src) << "Original content\n"; }
// --- Copy a file ---
fs::path dst = "demo_copy.txt";
fs::copy_file(src, dst, fs::copy_options::overwrite_existing);
// copy_options::overwrite_existing allows overwriting.
// Without it, copying to an existing file throws.
// Other options:
// skip_existing — silently skip if destination exists
// update_existing — overwrite only if source is newer
std::cout << std::format(" Copied {} -> {}\n", src.string(), dst.string());
// --- Rename (move) a file ---
fs::path renamed = "demo_renamed.txt";
fs::rename(dst, renamed);
std::cout << std::format(" Renamed {} -> {}\n", dst.string(), renamed.string());
// rename() works for both files and directories.
// It can also MOVE files across directories on the same filesystem.
// Moving across filesystems may fail — use copy + remove instead.
// --- File size ---
auto size = fs::file_size(src);
std::cout << std::format(" Size of {}: {} bytes\n", src.string(), size);
// --- Last write time ---
auto time = fs::last_write_time(src);
// Converting file_time to readable format is complex pre-C++20.
// We'll just show that we can retrieve it.
std::cout << " Last write time retrieved successfully\n";
// Cleanup
fs::remove(src);
fs::remove(renamed);
}5 Iterating directories
Iterating directories.
Use this when it cleanly solves the problem in front of you.
It improves correctness, clarity, and maintainability.
Use it in code like: void demo_directory_iteration() {.
void demo_directory_iteration() {
std::cout << "\n=== 5. Directory iteration ===\n\n";
// List the current directory (non-recursive)
std::cout << " Files in current directory (first 10):\n";
int count = 0;
for (const auto& entry : fs::directory_iterator(fs::current_path())) {
if (++count > 10) {
std::cout << " ... (truncated)\n";
break;
}
std::string type;
if (entry.is_directory()) type = "[DIR] ";
else if (entry.is_regular_file()) type = "[FILE]";
else type = "[????]";
std::cout << std::format(" {} {}\n", type,
entry.path().filename().string());
}
// --- Filtering by extension ---
// There's no built-in filter — use a simple if statement
std::cout << "\n .cpp files in current directory:\n";
for (const auto& entry : fs::directory_iterator(fs::current_path())) {
if (entry.is_regular_file() && entry.path().extension() == ".cpp") {
std::cout << std::format(" {}\n", entry.path().filename().string());
}
}
// For recursive search, use fs::recursive_directory_iterator instead.
}6 Space information
Space information.
Use this when it cleanly solves the problem in front of you.
It improves correctness, clarity, and maintainability.
Use it in code like: void demo_space_info() {.
void demo_space_info() {
std::cout << "\n=== 6. Disk space information ===\n\n";
auto info = fs::space(fs::current_path());
// space_info contains:
// capacity — total size of the filesystem
// free — free space (including reserved for root)
// available — free space available to non-privileged users
auto to_gb = [](std::uintmax_t bytes) {
return static_cast<double>(bytes) / (1024.0 * 1024.0 * 1024.0);
};
std::cout << std::format(" Capacity: {:.1f} GB\n", to_gb(info.capacity));
std::cout << std::format(" Free: {:.1f} GB\n", to_gb(info.free));
std::cout << std::format(" Available: {:.1f} GB\n", to_gb(info.available));
}7 Temporary directory and path utilities
Temporary directory and path utilities.
Use this when it cleanly solves the problem in front of you.
It improves correctness, clarity, and maintainability.
Use it in code like: void demo_temp_and_utils() {.
void demo_temp_and_utils() {
std::cout << "\n=== 7. Temp directory and utilities ===\n\n";
// --- Temporary directory ---
fs::path temp = fs::temp_directory_path();
std::cout << std::format(" Temp directory: {}\n", temp.string());
// --- Absolute and canonical paths ---
fs::path relative = ".";
fs::path absolute = fs::absolute(relative);
fs::path canonical = fs::canonical(relative);
// canonical() resolves symlinks and ".." — requires the path to exist
// absolute() just prepends the CWD — doesn't resolve symlinks
std::cout << std::format(" Relative: {}\n", relative.string());
std::cout << std::format(" Absolute: {}\n", absolute.string());
std::cout << std::format(" Canonical: {}\n", canonical.string());
// --- Relative path between two paths ---
fs::path from = "C:/Users/student";
fs::path to = "C:/Users/student/projects/app";
fs::path rel = fs::relative(to, from);
std::cout << std::format("\n Relative path from {} to {}:\n {}\n",
from.string(), to.string(), rel.string());
}Key Takeaways
- namespace fs = std::filesystem; — always alias the namespace.
- fs::path handles separators portably. Use / to join paths.
- Most functions have a throwing and an error_code overload.
- Use create_directories() (plural) for nested paths.
- remove_all() is recursive and dangerous — double-check paths.
- directory_iterator is non-recursive; recursive_directory_iterator traverses subdirectories.
- File system operations are subject to TOCTOU races — checking exists() then acting is never fully safe in concurrent programs.
int main() {
demo_paths();
demo_existence_checks();
demo_directory_operations();
demo_copy_rename();
demo_directory_iteration();
demo_space_info();
demo_temp_and_utils();
return 0;
}