std::filesystem
library.In this lesson, we’ll take a tour of the standard library functionality that helps us work with file systems in a platform-agnostic way.
The file system functionality is available by including <filesystem>
#include <filesystem>
Most of the functions and types we’ll be using are within the std::filesystem
namespace, which we’ll alias to fs
to keep our code less verbose:
namespace fs = std::filesystem;
A directory_entry
is the main type we use to refer to objects within our file system, such as files and directories. We can initialize a directory_entry
by passing the path to it on our hard drive.
#include <filesystem>
int main() {
namespace fs = std::filesystem;
fs::directory_entry Directory{"c:/test"};
}
Given paths often include backslashes, we typically populate them from raw strings. This means we don’t need to escape the back slashes, which keeps our paths readable:
#include <filesystem>
int main() {
namespace fs = std::filesystem;
fs::directory_entry Directory{R"(c:\test)"};
}
Directory entries have a lot of useful methods we can use to investigate the nature of what our path is pointing at.
For example, we can check if an entry exists at the path by using the exists()
method. We can also check if it is a directory or file by using the is_directory()
and is_regular_file()
 methods.
In the following example, c:\test
is a directory on our hard drive:
#include <filesystem>
#include <iostream>
int main() {
namespace fs = std::filesystem;
fs::directory_entry Directory{R"(c:\test)"};
if (Directory.exists()) {
std::cout << "The location exists";
}
if (Directory.is_directory()) {
std::cout << "\nIt is a directory";
}
if (!Directory.is_regular_file()) {
std::cout << "\nIt is not a file";
}
}
The location exists
It is a directory
It is not a file
Almost all of the files we’re familiar with on our hard drives - documents, images, executables, and more, are considered regular files.
Beyond directories and regular files, there are several other things that can exist within our file systems, such as block files and symbolic links. For this lesson, we’ll just focus on directories and regular files.
We can create a directory using the create_directory()
function, passing a path to the directory we want to create.
#include <filesystem>
int main() {
namespace fs = std::filesystem;
fs::create_directory(R"(c:\test)");
}
The function will return true
if the directory was created.
#include <filesystem>
#include <iostream>
int main() {
namespace fs = std::filesystem;
bool success{
fs::create_directory(R"(c:\test)")};
if (success) {
std::cout << "Directory created";
} else {
std::cout << "Directory not created";
}
}
Directory not created
There are two possible reasons the directory will not be created:
We’ll cover errors in more detail later in this lesson.
Sometimes, we’ll want to create a directory structure that is multiple levels deep. For example, we may want to create c:\test\subdirectory
, when c:\test
does not yet exist.
Using the create_directories
method, we can specify the exact directory we want, and if any intermediate directories are missing, they will be created too:
#include <filesystem>
int main() {
namespace fs = std::filesystem;
fs::create_directories(
R"(c:\deeply\nested\directory)");
}
Many of the functions in this lesson can throw exceptions. We can catch these in the normal way, using a try-catch block. The type of exception thrown by file system errors is std::filesystem::filesystem_error
.
Like any standard library exception, they have a what()
method that we can use to get a description of the error.
For more programmatic error handling, we’re likely to want to use the code()
method instead. This returns a numeric error code that is much easier to work with than the string output of what()
#include <filesystem>
#include <iostream>
int main() {
namespace fs = std::filesystem;
try {
fs::create_directory(R"(k:\fake)");
} catch (fs::filesystem_error e) {
std::cout << e.code() << '\n';
std::cout << e.what();
}
}
system:3
create_directory: The system cannot find
the path specified.: "k:\fake"
The meaning of error codes depends on the operating system our program is running on. The previous example is from Windows, which uses the following error codes: https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes
file_size()
When our directory_entry
is pointing at a file, we can use the file_size
method to return its size in bytes:
#include <filesystem>
#include <iostream>
int main() {
namespace fs = std::filesystem;
fs::directory_entry File{
R"(c:\test\hello.txt)"};
std::cout << "File Size: " << File.file_size()
<< " bytes";
}
File Size: 24 bytes
last_write_time()
We can find the time a file or directory was last modified by using the last_write_time()
method on our directory_entry
:
#include <filesystem>
#include <iostream>
int main() {
namespace fs = std::filesystem;
fs::directory_entry File{
R"(c:\test\hello.txt)"};
std::cout << "Last Write Time:\n"
<< File.last_write_time();
}
Last Write Time:
2023-06-10 23:10:33.4202407
copy_file
We can copy files using the copy_file
method, with two arguments. The first argument is the path to the file we want to copy. The second argument is the path we want to copy it to:
#include <filesystem>
int main() {
namespace fs = std::filesystem;
fs::copy_file(R"(c:\test\hello.txt)",
R"(c:\test\hi.txt)");
We can pass an additional third argument to copy_file
, which lets us define how our code should behave if the path defined by the second argument already exists.
In such a scenario, our copy_file
call risks overwriting a file.
The std::filesystem::copy_options
enumeration contains our options for handling this. They are:
none
- keep the existing file and throw an exception (default)skip_existing
- keep the existing file but do not throw an exceptionoverwrite_existing
- overwrite the existing fileupdate_existing
- overwrite the existing file if it’s older than the new file, as defined by the last_write_time
#include <filesystem>
int main() {
namespace fs = std::filesystem;
fs::copy_file(
R"(c:\test\hello.txt)",
R"(c:\test\hi.txt)",
fs::copy_options::skip_existing);
}
copy
We can copy a directory with the copy
 method.
Similar to copy_file
, we can pass options to define how the copy
action works. When dealing with overwriting existing files, we have the same 4 options we had with copy_file
.
We can also pass up to two additional options that take effect when what we’re copying is a directory:
recursive
- Recursively copy subdirectoriesdirectories_only
- Only copy the directory structure - ignore filesBoth options are disabled by default, but we can enable them as needed, and we can enable multiple options by combining them with the |
 operator:
#include <filesystem>
#include <iostream>
int main() {
namespace fs = std::filesystem;
fs::copy(
R"(c:\test)", R"(c:\test2)",
fs::copy_options::skip_existing |
fs::copy_options::recursive |
fs::copy_options::directories_only);
}
Additional copy
options are available when we’re dealing with things like symbolic links, but that’s out of scope for now.
Note, the copy
function can copy a file - it is not restricted to just copying a directory. However, if we're copying a file, the copy_file
method makes our intent much clearer and has built in error-checking to ensure the target actually is a file. So, when copying a file, using the copy_file
method tends to be recommended.
remove()
We can remove a file or directory using the remove
 function:
#include <filesystem>
int main() {
namespace fs = std::filesystem;
fs::remove(R"(c:\test)");
}
When the path we provide points to a directory that is not empty, an exception is thrown
#include <filesystem>
#include <iostream>
int main() {
namespace fs = std::filesystem;
try {
fs::remove(R"(c:\test)");
} catch (fs::filesystem_error e) {
std::cout << "Code: " << e.code()
<< "\nError: " << e.what();
}
}
Code: system:145
Error: remove: The directory is not empty.: "c:/test"
When we’re trying to remove a directory, and we want to get rid of everything inside it too, we can use the remove_all
 method.
remove_all()
The remove_all()
method removes a directory and all of its contents. It returns an integer, representing how many files and directories were removed.
#include <filesystem>
#include <iostream>
int main() {
namespace fs = std::filesystem;
uintmax_t filesDeleted{
fs::remove_all(R"(c:\test)")};
std::cout << "Deleted " << filesDeleted
<< " files or directories";
}
Deleted 2 files or directories
rename()
We can move or rename a file or directory using the rename()
function. The first argument will be a path to the thing we want to move. The second argument will be the location we want to move it to.
#include <filesystem>
int main() {
namespace fs = std::filesystem;
fs::rename(R"(c:\test)", R"(c:\test2)");
}
space()
The space
function lets us get more information about the storage space associated with a path. It returns a space_info
struct, which has three fields:
capacity
- The total capacity available at the locationfree
- The amount of space free at the locationavailable
- The amount of space that is available for our program to write to. This will be less than or equal to the free
spaceAll three values are in bytes:
#include <filesystem>
#include <iostream>
int main() {
namespace fs = std::filesystem;
const fs::space_info si{fs::space(R"(c:\)")};
constexpr int bytesInGB{1024 * 1024 * 1024};
std::cout << "Capacity: "
<< si.capacity / bytesInGB << "GB";
std::cout << "\nFree: " << si.free / bytesInGB
<< "GB";
std::cout << "\nAvailable: "
<< si.available / bytesInGB << "GB";
}
Capacity: 437GB
Free: 277GB
Available: 277GB
std::filesystem::path
In the previous examples, we’re representing our paths as simple strings, but the underlying type is a std::filesystem::path
. This type has some additional useful functions specific to this use case.
path
objects can be created from simple strings, which is what our code has been doing so far:
#include <filesystem>
int main() {
namespace fs = std::filesystem;
fs::path Location{R"(c:\test)"};
}
We can get the string representation of a path using the string()
method, which is useful when we want to display it:
#include <filesystem>
#include <iostream>
int main() {
namespace fs = std::filesystem;
fs::path Location{R"(c:\test)"};
std::cout << Location.string();
}
c:\test
We can also get the path associated with a directory entry using the path()
 method:
#include <filesystem>
#include <iostream>
int main() {
namespace fs = std::filesystem;
fs::directory_entry Entry{R"(c:\test)"};
std::cout << Entry.path().string();
}
c:\test
path
methodsA variety of methods give us access to specific parts of the path. These also return path
objects, so in the following example we use the string
method to display them:
#include <filesystem>
#include <iostream>
int main() {
namespace fs = std::filesystem;
fs::path Location{R"(c:\test\hello.txt)"};
std::cout << "File Name: "
<< Location.filename().string();
std::cout << "\nFile Stem: "
<< Location.stem().string();
std::cout << "\nFile Extension: "
<< Location.extension().string();
std::cout << "\nParent Path: "
<< Location.parent_path().string();
std::cout << "\nRoot Path: "
<< Location.root_name().string();
}
File Name: hello.txt
File Stem: hello
File Extension: .txt
Parent Path: c:\test
Root Path: c:
Equivalent boolean methods are available that return true
or false
based on the existence of any of these path parts.
We can access these by prepending has_
to the method names. For example, has_filename()
and has_extension()
#include <filesystem>
#include <iostream>
int main() {
namespace fs = std::filesystem;
fs::path Directory{R"(c:\test\)"};
if (!Directory.has_filename()) {
std::cout << Directory.string()
<< " has no file name\n";
}
fs::path File{R"(c:\hi.txt)"};
if (File.has_extension()) {
std::cout << File.string()
<< " has a file extension";
}
}
Note the result of these functions is based entirely on the format of the provided string.
These methods do not check the underlying file system. For example, the has_filename
method returns true
if it seems like the provided path has a file name. But, to check what actually exists at that location, we will need to create a directory_entry
, not just a path
.
path
file namesWhen working with paths, a common requirement is manipulate the file name, so we have some methods to help us there:
#include <filesystem>
#include <iostream>
int main() {
namespace fs = std::filesystem;
fs::path File{R"(c:\test\hello.txt)"};
std::cout << File.string() << '\n';
File.replace_filename("world.txt");
std::cout << File.string() << '\n';
File.replace_extension("doc");
std::cout << File.string() << '\n';
File.remove_filename();
std::cout << File.string();
}
c:\test\hello.txt
c:\test\world.txt
c:\test\world.doc
c:\test\
Use cases for these methods typically come up when we're trying to rename or copy files that have paths we don't necessarily know in advance. For example, we can be creating a function that accepts a path as an argument:
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
void CreateBackup(const fs::path& Path) {
fs::path Backup{Path};
Backup.replace_extension("backup");
fs::copy_file(Path, Backup);
}
int main() {
CreateBackup(R"(c:\test\hello.txt)");
}
The path
type also overrides the /=
operator, which allows us to create paths to subdirectories or files, by automatically appending separators that are appropriate to the operating system.
#include <filesystem>
#include <iostream>
int main() {
namespace fs = std::filesystem;
fs::path File{R"(c:\)"};
std::cout << File.string() << '\n';
File /= "test";
std::cout << File.string() << '\n';
File /= "hello.txt";
std::cout << File.string() << '\n';
}
c:\
c:\test
c:\test\hello.txt
These methods return a reference to the original object, allowing them to be chained:
#include <filesystem>
#include <iostream>
int main() {
namespace fs = std::filesystem;
fs::path File{R"(c:\test\hello.txt)"};
std::cout << File.string() << '\n';
File.remove_filename() /= "subdirectory";
std::cout << File.string();
}
c:\test\hello.txt
c:\test\subdirectory
All the paths we’ve shown so far have been absolute paths, but we can also use relative paths.
We can check if a path is relative or absolute using the is_relative
and is_absolute
 methods.
#include <filesystem>
#include <iostream>
int main() {
namespace fs = std::filesystem;
fs::path A{R"(c:\test\hello.txt)"};
if (A.is_absolute()) {
std::cout << "A is Absolute";
}
fs::path B{R"(hello.txt)"};
if (B.is_relative()) {
std::cout << "B is Relative";
}
}
A is Absolute
B is Relative
Relative paths are relative to some other directory, sometimes called the “current path” or “current working directory”
We can see the current working directory using the current_path
method. Its default value depends on our settings, but we can pass a new path to that function, to set what our working directory should be.
#include <filesystem>
#include <iostream>
int main() {
namespace fs = std::filesystem;
fs::directory_entry Entry{R"(hello.txt)"};
std::cout
<< "The default working directory is:\n"
<< fs::current_path().string();
if (!Entry.exists()) {
std::cout << "\nThe file was not found\n\n";
}
fs::current_path(R"(c:\test)");
std::cout << "The current working directory "
"was changed to:\n"
<< fs::current_path().string();
if (Entry.exists()) {
std::cout << "\nThe file was found!";
}
}
The default working directory is:
C:\Users\ryanm\repos\cpp-tuts
The file was not found
The current working directory was changed to:
c:\test
The file was found!
directory_iterator
We can navigate through a directory using a directory_iterator
 pair.
By passing a path to this type, we can define the directory we want to iterate through. This will act as our starting point.
The default constructor for directory_iterator
returns an end iterator. We can increment our starting iterator and, once it becomes equal to the default-constructed iterator, we know we have navigated through the entire directory.
#include <filesystem>
#include <iostream>
int main() {
namespace fs = std::filesystem;
fs::directory_iterator Start{R"(c:\test)"};
fs::directory_iterator End{};
for (auto Iter{Start}; Iter != End; ++Iter) {
std::cout << Iter->path().string();
if (Iter->is_directory()) {
std::cout << " (Directory)";
} else if (Iter->is_regular_file()) {
std::cout << " (" << Iter->file_size()
<< " Bytes)";
}
std::cout << '\n';
}
}
c:\test\hello.txt (24 Bytes)
c:\test\subdirectory (Directory)
c:\test\world.txt (62 Bytes)
By default, the directory_iterator
only iterates through the first level of entries within our directory. If an entry is a subdirectory, we will not navigate into it.
We could build this logic ourselves, by implementing recursive behavior when is_directory
returns true. However, the standard library has built this for us, in the form of the recursive_directory_iterator
recursive_directory_iterator
This iterator works in much the same way as the directory_iterator
, except it will navigate into any subdirectories it finds along the way.
The following code is identical to the previous example. Only the highlighted lines have been changed to switch to a recursive iterator.
#include <filesystem>
#include <iostream>
int main() {
namespace fs = std::filesystem;
fs::recursive_directory_iterator Start{
R"(c:\test)"};
fs::recursive_directory_iterator End{};
for (auto Iter{Start}; Iter != End; ++Iter) {
std::cout << Iter->path().string();
if (Iter->is_directory()) {
std::cout << " (Directory)";
} else if (Iter->is_regular_file()) {
std::cout << " (" << Iter->file_size()
<< " Bytes)";
}
std::cout << '\n';
}
}
We now navigate into subdirectories:
c:\test\hello.txt (24 Bytes)
c:\test\subdirectory (Directory)
c:\test\subdirectory\deep.txt (10 Bytes)
c:\test\world.txt (62 Bytes)
The recursive iterator has an additional method, called depth()
. This will return the depth of the recursion, ie, how deeply nested the current directory entry is, relative to our starting point:
#include <filesystem>
#include <iostream>
int main() {
namespace fs = std::filesystem;
using std::cout, std::setw;
fs::recursive_directory_iterator Start{
R"(c:\test)"};
fs::recursive_directory_iterator End{};
for (auto Iter{Start}; Iter != End; ++Iter) {
std::cout << "Depth " << Iter.depth()
<< " - " << Iter->path().string()
<< '\n';
}
}
Depth 0 - c:\test\hello.txt
Depth 0 - c:\test\subdirectory
Depth 1 - c:\test\subdirectory\deep.txt
Depth 0 - c:\test\world.txt
Up next, we’ll start working with file streams. These are the mean way we create, read, and update files within our file systems.
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.