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;
namespace fs = std::filesystem;Frequently Asked Questions
What IS a "path"? Is it a string?
Forward slash or backslash?
Do these operations throw exceptions?
Is std::filesystem thread-safe?
What's the difference between "path" and "canonical path"?
How 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
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
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
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
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
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
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
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;
}