iostream formatting

std::filesystem is the standard library's view of the file system: paths, directories and file metadata. It replaces the platform calls of POSIX and Windows with one set of names that compile on both.

The namespace is long, so programs write namespace fs = std::filesystem;. The functions come in two forms: one throws on failure and one takes a std::error_code, which is the form for a program that wants to handle a missing file without exceptions.

The path type also normalises: it collapses a trailing slash, treats an empty component as a separator and compares lexically. That is why a program can build a path from user input and hand it to a function without reimplementing the platform's rules.

Paths

A fs::path stores a path and knows its parts. The / operator appends a component with the right separator for the platform, and filename(), stem(), extension() and parent_path() read the pieces back.

The program builds data/input.txt from two parts and prints the path and its parts. The separator is a slash on Linux and a backslash on Windows, which is why the operator exists.

A path converts to std::string with string() and to a C string with c_str(). The functions that open files accept a path directly, so the conversion is only needed when a call wants a string.

#include <filesystem> #include <iostream> namespace fs = std::filesystem; int main() { fs::path p = fs::path{"data"} / "input.txt"; std::cout << p << '\n'; std::cout << p.filename() << ' ' << p.extension() << ' ' << p.parent_path() << '\n'; std::cout << p.stem() << '\n'; }

g++ -std=c++23 -Wall -Wextra path.cpp -o path_demo
./path_demo

"data/input.txt"
"input.txt" ".txt" "data"
"input"

Creating directories

create_directories creates every missing part of a path and returns false when the directory already existed. create_directory creates one level and fails if the parent is missing.

The program creates work/sub, which needs two levels, and prints whether the result exists and is a directory. Both answers are 1.

The functions that report a bool are the quiet form; the throwing form reports the same failure as an exception. A program that creates a temporary directory usually uses the error_code overload, which the last section shows.

#include <filesystem> #include <iostream> namespace fs = std::filesystem; int main() { fs::path dir = fs::path{"work"} / "sub"; fs::create_directories(dir); std::cout << fs::exists(dir) << ' ' << fs::is_directory(dir) << '\n'; }

g++ -std=c++23 -Wall -Wextra create.cpp -o create_dirs
./create_dirs

1 1

Listing a directory

directory_iterator walks the entries of one directory. Each entry has a path(), a file type and, when asked, a status that tells whether it is a regular file or a directory.

The program creates a directory with two files and collects the file names in a vector, then sorts them. Sorting is the program's job because the order of the iterator is unspecified.

recursive_directory_iterator walks subdirectories as well. Both iterators skip the . and .. entries, and both have an error_code overload for a directory that may not exist.

#include <algorithm> #include <filesystem> #include <fstream> #include <iostream> #include <string> #include <vector> namespace fs = std::filesystem; int main() { fs::create_directories("listing"); std::ofstream{"listing/a.txt"} << "a"; std::ofstream{"listing/b.log"} << "b"; std::vector<std::string> names; for (const auto& entry : fs::directory_iterator("listing")) { names.push_back(entry.path().filename().string()); } std::sort(names.begin(), names.end()); for (const auto& name : names) { std::cout << name << ' '; } std::cout << '\n'; }

g++ -std=c++23 -Wall -Wextra iterate.cpp -o iterate
./iterate

a.txt b.log

File size

file_size returns the size in bytes and follows symbolic links. It throws when the file does not exist, which is why a program that may be given a missing path uses the error_code form.

The program writes five characters and asks for the size. The answer is 5, and it is the size on disk after the stream flushed and closed.

The same call on a directory is an error on most systems, and on Linux the size of a directory is the size of its entries block, which is not the sum of the files. Use it for files, not for what a directory contains.

#include <filesystem> #include <fstream> #include <iostream> namespace fs = std::filesystem; int main() { std::ofstream{"size.txt"} << "12345"; std::cout << fs::file_size("size.txt") << '\n'; }

g++ -std=c++23 -Wall -Wextra size.cpp -o size_demo
./size_demo

5

The error_code overload

Every filesystem function has an overload that takes a std::error_code as the last argument and does not throw. On failure the function returns a default value and the code says what went wrong.

The program asks for the size of a missing file with an error_code. The size is 0 and the message is the operating system's, which comes from the C library and is the same text a shell would print.

This is the form to use in a program that treats a missing file as ordinary, such as a cache lookup. The throwing form is for the case where the file has to be there.

#include <filesystem> #include <iostream> #include <system_error> namespace fs = std::filesystem; int main() { std::error_code ec; auto size = fs::file_size("missing.txt", ec); std::cout << size << ' ' << ec.message() << '\n'; }

g++ -std=c++23 -Wall -Wextra errors.cpp -o fs_errors
./fs_errors

18446744073709551615 No such file or directory

Exercises

  1. Build the path of a file three levels down with / and print parent_path() at each level.
  2. List a directory with recursive_directory_iterator and print the size of every regular file.
  3. Remove a directory tree with remove_all and print the number of entries it removed.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does std::filesystem::path represent?

    Answer

    A path, not a file

    The path is a value built from strings, and the functions act on it.

  2. What does path.filename() return?

    Answer

    The last component of the path

    The filename is the name of the entry, and stem and extension split it further.

  3. What does create_directories do?

    Answer

    It creates every missing directory in the path

    The plural function builds the whole chain and reports whether it created anything.

  4. What does directory_iterator give?

    Answer

    An entry per item in one directory

    The iterator walks one level; recursive_directory_iterator walks the tree.

  5. What does this program print? #include <filesystem> #include <iostream> namespace fs = std::filesystem; int main() { fs::path p = "data/input.txt"; std::cout << p << '\n'; std::cout << p.filename() << ' ' << p.extension() << ' ' << p.parent_path() << '\n'; std::cout << p.stem(); }

    Answer

    the path, then filename/extension/parent, then the stem

    The path prints quoted, and each accessor returns the corresponding part.

  6. What does this program print? #include <filesystem> #include <iostream> namespace fs = std::filesystem; int main() { std::cout << fs::create_directories("one/two") << ' '; std::cout << fs::create_directories("one/two") << ' '; std::cout << fs::exists("one/two"); fs::remove_all("one"); }

    Answer

    1 0 1

    The first call creates both directories, the second finds them, and exists reports the result.

  7. What does this program print? #include <filesystem> #include <fstream> #include <iostream> #include <string> #include <vector> #include <algorithm> namespace fs = std::filesystem; int main() { fs::create_directories("scan"); { std::ofstream{"scan/a.txt"} << "x"; } { std::ofstream{"scan/b.log"} << "y"; } std::vector<std::string> names; for (const auto& entry : fs::directory_iterator{"scan"}) names.push_back(entry.path().filename().string()); std::sort(names.begin(), names.end()); for (const auto& name : names) std::cout << name << ' '; fs::remove_all("scan"); }

    Answer

    a.txt b.log

    The directory holds the two files, and the sorted names are printed.

  8. What does this program print? #include <filesystem> #include <fstream> #include <iostream> namespace fs = std::filesystem; int main() { { std::ofstream out{"size.txt"}; out << "12345"; } std::cout << fs::file_size("size.txt"); fs::remove("size.txt"); }

    Answer

    5

    The file holds five bytes, and file_size reports the byte count.

  9. What does this program print? #include <filesystem> #include <iostream> namespace fs = std::filesystem; int main() { std::error_code error; auto size = fs::file_size("missing.txt", error); std::cout << size << ' ' << error.message(); }

    Answer

    the maximum size value and the error message

    The error_code overload returns the maximum value and fills the code, and the message comes from the system.

  10. Why take an error_code overload instead of catching an exception?

    Answer

    The failure becomes part of the normal control flow

    The program can report a missing file without an exception on every check.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev