Working with the File System

Create, delete, move, and navigate through directories and files using the std::filesystem library.
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
3D Concept Art of a Dragon
Ryan McCombe
Ryan McCombe
Updated

In this lesson, we’ll tour 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{"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 file

"Regular" Files?

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, several other things 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.

Getting 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 bytes

Getting 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:

#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.4202407

Creating 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 created

There 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 exist

Copying 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 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 exception
  • overwrite_existing - overwrite the existing file
  • update_existing - overwrite the existing file if it’s older than the new file, as defined by the last_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);
}

Creating New Files

The std::filesystem library isn’t designed for directly creating new files. Later in this chapter, we introduce file streams, which are the typical mechanisms we use for creating and manipulating files.

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 subdirectories
  • directories_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. We covered this in more detail in our earlier course:

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 directories

Moving and Renaming Files and Directories

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>
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 location
  • free - The amount of space free at the location
  • available - The amount of space that is available for our program to write to. This will be less than or equal to the free space

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: 277GB

Summary

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_entry to 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.

Was this lesson useful?

Next Lesson

File System Paths

A guide to effectively working with file system paths, using the path type within the standard library's filesystem module.
3D Character Concept Art
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Files and Serialization
Next Lesson

File System Paths

A guide to effectively working with file system paths, using the path type within the standard library's filesystem module.
3D Character Concept Art
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved