Working with the File System
Create, delete, move, and navigate through directories and files using the std::filesystem library.
In this lesson, we'll explore 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;Directory Entry
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>
namespace fs = std::filesystem;
int main() {
fs::directory_entry Directory{R"(c:/test)"};
}Given paths often include backslashes, which denote escape sequences in strings, we need to give them extra consideration. When we want a string to contain a literal \, we need to escape it with an additional \, so the string representing a path like c:\test would use the string "c:\\test"
Alternatively, we can use raw strings, which allow us to provide our path as-is:
#include <filesystem>
namespace fs = std::filesystem;
int main() {
// Using a regular string
fs::directory_entry A{"c:\\test"};
// Using a raw string
fs::directory_entry B{R"(c:\test)"};
}The raw string approach tends to be more readable, so it's what we'll use in the rest of this lesson.
Directory Entry Methods
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>
namespace fs = std::filesystem;
int main() {
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 fileGetting File Size with 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>
namespace fs = std::filesystem;
int main() {
fs::directory_entry File{
R"(c:\test\hello.txt)"};
std::cout << "File Size: " << File.file_size()
<< " bytes";
}File Size: 24 bytesGetting the last modified time with 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. In C++17, the type returned from this function was not explicitly specified - it depended on the underlying implementation. A minimalist example looked like the following, but may not work across all compilers and platforms:
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
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.4202407In C++20, this was made more rigid. A dedicated std::chrono::file_clock type was added for this purpose, and the last_write_time() method now returns a std::chrono::time_point using that clock. For convenience, an alias for this type is available as std::filesystem::file_time_type.
A portable, C++20 implementation might look something like this:
#include <filesystem>
#include <iostream>
#include <format> // for std::format
#include <chrono> // for fs::file_time_type
namespace fs = std::filesystem;
int main() {
fs::directory_entry File{R"(c:\test\hello.txt)"};
// std::filesystem::file_time_type is an alias for
// std::chrono::time_point<std::chrono::file_clock>
fs::file_time_type Time{File.last_write_time()};
std::cout << std::format(
"Last Write Time: {}", Time
);
}Last Write Time:
2023-06-10 23:10:33.4202407Creating a Directory
We can create a directory using the create_directory() function, passing a path to the directory we want to create.
#include <filesystem>
namespace fs = std::filesystem;
int main() {
fs::create_directory(R"(c:\test)");
}The function will return true if the directory was created.
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
if (fs::create_directory(R"(c:\test)")) {
std::cout << "Directory created";
} else {
std::cout << "Directory not created";
}
}Directory not createdThere are two possible reasons the directory will not be created:
- it already exists, as in this case, or
- an error was reported from the operating system
We'll cover errors in more detail later in this lesson.
Deeply Creating a Directory
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>
namespace fs = std::filesystem;
int main() {
fs::create_directories(
R"(c:\deeply\nested\directory)");
}File System Exceptions and Errors
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 will likely want to use the code() method instead. This returns a std::error_code object, that is much easier to work with programmatically.
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
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, whose error codes are available on the official site.
We can access the integer error code using the value() method:
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
try {
fs::create_directory(R"(k:\fake)");
} catch (fs::filesystem_error& e) {
if (e.code().value() == 3) {
std::cout << "Path doesn't exist";
}
}
}Path doesn't existCopying Files and Directories
There are two main functions we use for copying files and directories - copy_file() and copy()
Copying Files with 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>
namespace fs = std::filesystem;
int main() {
fs::copy_file(R"(c:\test\hello.txt)",
R"(c:\test\hi.txt)");
}We can pass an additional third argument to copy_file(), which allows us to define how our code should behave if the path specified 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 thelast_write_time()
#include <filesystem>
namespace fs = std::filesystem;
int main() {
fs::copy_file(
R"(c:\test\hello.txt)",
R"(c:\test\hi.txt)",
fs::copy_options::skip_existing);
}Copying a Directory with copy()
We can copy a directory with the copy() function.
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 files
Both 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>
namespace fs = std::filesystem;
int main() {
fs::copy(
R"(c:\test)", R"(c:\test2)",
fs::copy_options::skip_existing |
fs::copy_options::recursive |
fs::copy_options::directories_only);
}The | syntax in the previous example is a bitwise operator. This was covered this in more detail in our earlier course:
Bitwise Operators and Bit Flags
Unravel the fundamentals of bitwise operators and bit flags in this practical lesson
Additional copy() options are available when we're dealing with things like symbolic links, but that's out of scope for now.
Note, that 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.
Additionally, copy_file() has built-in error-checking to ensure the target really is a file.
Deleting Files and Directories
There are two functions we can use to delete files and directories: remove() and remove_all()
Removing a File or Directory using remove()
We can remove a file or directory using the remove() function:
#include <filesystem>
namespace fs = std::filesystem;
int main() {
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>
namespace fs = std::filesystem;
int main() {
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"Removing a directory and all contents using remove_all()
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. It returns an integer, representing how many files and directories were removed.
The specific type returned is uintmax_t, a fixed-width integer that can be used like any other integer type.
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
uintmax_t FilesDeleted{
fs::remove_all(R"(c:\test)")};
std::cout << "Deleted " << FilesDeleted
<< " files or directories";
}Deleted 2 files or directoriesMoving and Renaming Files and Directories
We can move or rename a file or directory using the rename() function. The first argument is the path to the item we want to move. The second argument will be the location we want to move it to.
#include <filesystem>
namespace fs = std::filesystem;
int main() {
fs::rename(R"(c:\test)", R"(c:\test2)");
}Getting Disk 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 thefreespace
All three values are in bytes:
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
auto [capacity, free, available]{
fs::space(R"(c:\)")};
constexpr int bytesInGB{1024 * 1024 * 1024};
std::cout << "Capacity: "
<< capacity / bytesInGB << "GB"
<< "\nFree: " << free / bytesInGB
<< "GB\nAvailable: "
<< available / bytesInGB << "GB";
}Capacity: 437GB
Free: 277GB
Available: 277GBSummary
This lesson provided an overview of the C++ filesystem library to effectively manage files and directories on the user's computer. The key topics we covered included:
- Learned to include and use the
<filesystem>library. - Understood the use of
directory_entryto interact with file system objects. - Explored methods to check the existence and type of files and directories.
- Gained skills in creating, copying, and removing files and directories.
- Learned how to handle file system errors and exceptions.
File System Paths
A guide to effectively working with file system paths, using the std::filesystem::path type.