Chapter 08: File System

The file system library provides functions related to the operation of the file system, path, regular files, directories, and so on. Similar to the regular expression library, it was one of the first libraries to be launched by boost and eventually merged into the C++ standard in C++17. Its entire contents live in the <filesystem> header under the std::filesystem namespace, for which a short alias is conventionally introduced:

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

On some older toolchains (e.g. GCC before version 8), using the filesystem library also required linking against -lstdc++fs (or LLVM's -lc++fs); modern compilers no longer need this.

8.1 The path std::filesystem::path

std::filesystem::path is the heart of the library. It represents a file path in a portable way and hides the differences between operating systems in their path separators (such as / and \). A path can be constructed from a string and composed with operator/:

fs::path p = "/usr/local";
p /= "bin"; // p is now /usr/local/bin
fs::path q = p / "clang"; // composes without modifying p

It is worth emphasizing that a path is only a syntactic representation of a path: constructing one neither touches the disk nor requires the path to actually exist. It provides a set of member functions for decomposing a path:

fs::path p = "/usr/local/hello.txt";
p.filename(); // "hello.txt"
p.stem(); // "hello"
p.extension(); // ".txt"
p.parent_path(); // "/usr/local"

8.2 Querying file status

The library provides a set of non-member functions for querying the actual file a path refers to; each takes a path argument:

fs::exists(p);            // does the path exist
fs::is_regular_file(p); // is it a regular file
fs::is_directory(p); // is it a directory
fs::file_size(p); // file size in bytes
fs::last_write_time(p); // last modification time

Note that these operations, which genuinely access the file system, throw a std::filesystem::filesystem_error on failure (e.g. the path does not exist or permission is denied). For nearly every such function the library provides an overload taking a std::error_code&, to report errors without exceptions:

std::error_code ec;
auto size = fs::file_size(p, ec); // does not throw on error; writes to ec
if (ec) {
std::cout << "cannot get size: " << ec.message() << std::endl;
}

8.3 Iterating directories

std::filesystem::directory_iterator iterates over the immediate entries of a directory, while recursive_directory_iterator walks the entire directory tree. Both can be used directly in a range-based for loop, yielding directory_entry elements:

for (const auto& entry : fs::directory_iterator(dir)) {
std::cout << entry.path() << std::endl;
}

// recursively walk the whole tree
for (const auto& entry : fs::recursive_directory_iterator(dir)) {
if (entry.is_regular_file())
std::cout << entry.path() << " (" << entry.file_size() << ")\n";
}

A directory_entry caches the status of the file it refers to, so member functions such as entry.is_regular_file() and entry.file_size() are often more efficient than calling the non-member functions again on its path.

8.4 Creating, copying, and removing

The library also offers a set of operations that modify the file system:

fs::create_directories(p / "a" / "b"); // create directories recursively
fs::copy_file(src, dst); // copy a single file
fs::copy(src, dst, fs::copy_options::recursive); // copy a directory recursively
fs::rename(old_path, new_path); // rename / move
fs::remove(p); // remove a single file or empty directory
fs::remove_all(p); // remove recursively, returns the count removed

8.5 A complete example

The following example combines the operations above. To keep it self-contained and repeatable, we create a dedicated working directory under the system temporary directory and clean it up at the end:

#include <filesystem>
#include <fstream>
#include <iostream>

namespace fs = std::filesystem;

int main() {
// Work in a private scratch directory so the example is
// self-contained and repeatable.
const fs::path base = fs::temp_directory_path() / "modern-cpp-fs-demo";
fs::remove_all(base); // clean up any previous run
fs::create_directories(base / "sub"); // creates intermediate dirs

// Create a file.
std::ofstream(base / "hello.txt") << "hello, filesystem";

// Path decomposition (no filesystem access required).
const fs::path p = base / "hello.txt";
std::cout << "filename: " << p.filename() << "\n";
std::cout << "extension: " << p.extension() << "\n";
std::cout << "parent: " << p.parent_path() << "\n";

// Query the file.
std::cout << "exists: " << fs::exists(p) << "\n";
std::cout << "is_regular_file: " << fs::is_regular_file(p) << "\n";
std::cout << "file_size: " << fs::file_size(p) << "\n";

// Recursively iterate the directory tree.
std::cout << "entries:\n";
for (const auto& entry : fs::recursive_directory_iterator(base))
std::cout << " " << entry.path() << "\n";

// Copy, then rename.
fs::copy_file(p, base / "copy.txt");
fs::rename(base / "copy.txt", base / "renamed.txt");

// Clean up.
fs::remove_all(base);
std::cout << "after cleanup, exists: " << fs::exists(base) << "\n";
}

Further Readings