How hard can it be to remove a folder in Windows? Part 1: Using C++ standard libraries

Introduction

I recently worked on a project where one of the simplest task was to remove a folder in Windows using C++. The only catch was that it needed to be robust and really do a good attempt to remove the folder. How hard can it be?

Using standard libraries

The good news is that there are standard libraries for doing just that in the std::filesystem namespace since C++17. These libraries are based on the boost::filesystem libraries that have been available for a long time in boost. And the Microsoft C++ compiler included them in the std::experimental::filesystem namespace well before 2017.

The functions to remove an entry from the filesystem given a path look like:

1
2
bool remove(const std::filesystem::path& p);
bool remove(const std::filesystem::path& p, std::error_code& ec) noexcept;

There are two versions: the first one receives just the path. There are two possible outcomes to start with. The function either succeeds, in which case it returns, or it fails, in which case it throws an exception. The success case also has two possible outcomes. If the entry existed and was deleted the function returns true. If the file did not exist it returns false.

The underlying OS APIs that delete an entry usually provide the information that the entry existed without the need for an additional OS API call to check existence. E.g. on Windows DeleteFileW sets the error to ERROR_FILE_NOT_FOUND or ERROR_PATH_NOT_FOUND if the file did not exist, which can be translated into a success, but returning false from remove.

The second version receives the path and a reference to an error code. If the function fails, instead of throwing it sets the error code. The function is marked noexcept, which in this case means that it will never throw, it will always return. When the error code was set, the return value is false. In general, for this style of interface, when the error code is set the return value is the default constructed one (false is the default constructed value for bool).

In both cases the input path can be a file or a folder, but the folder must be empty, otherwise the functions will fail.

It turns out that in our case we want to also be able to remove folders that are not empty.

There is a pair of functions however that can deal with a folder that is not empty, in a recursive fashion if necessary.

1
2
std::uintmax_t remove_all(const std::filesystem::path& p);
std::uintmax_t remove_all(const std::filesystem::path& p, std::error_code& ec);

The return value is the number the number of entries deleted, zero if the path did not exist to start with.

The error handling for remove_all looks similar to the remove functions. However, notice that the version that receives an error code is not marked as noexcept. Unlike remove that just passes the path as a zero-terminated string to the underlying OS provided C function, remove_all has to enumerate and concatenate children paths, hence it needs to allocate memory.

It turns out that the version of remove_all that receives an error_code has two kinds of failures. For most of the errors it will set the error code. However it can throw std::bad_alloc if it fails to allocate the memory it needs. The rationale is that translating memory exceptions to error codes is very tedious.

Unfortunately if we use std::filesystem::remove_all to delete a folder with many files, if it encounters an error with one file, it stops there without attempting to delete the remaining ones.

In the case of our application we would like to delete as many files as possible from the folder because it’s easier to diagnose failures if the files with errors are the only ones left behind.

The bad news is that up to this point we’ve spent time with functions we can’t use, we’ve got to write a custom function where we enumerate the contents of the folder and continue on errors.