Filesystem in C++17

Gettting started with Experimental Filesystem Features C++17 (g++)

We just have to "tell" compiler that we write C++17 code (-c++1z) and it has to add standard library with filesystem library (-lstdc++fs).

g++ -std=c++1z main.cpp -lstdc++fs && ./a.out

Let's see a simple example with std::filesystem::path class.

#include <experimental/filesystem>
#include <iostream>

namespace fs = std::experimental::filesystem;
using namespace std;

int main()
{
    fs::path aPath {"./path/to/file.txt"};

    cout << "Parent path: " << aPath.parent_path() << endl;
    cout << "Filename: " << aPath.filename() << endl;
    cout << "Extension: " << aPath.extension() << endl;

    return 0;
}

Compile and run: Basic C++17 example

Run output is:

$ g++ -std=c++1z main.cpp -lstdc++fs && ./a.out
$ ./a.out

Parent path: "./path/to"
Filename: "file.txt"
Extension: ".txt"

C++17 Filesystem Features

In this section, we are going to explain some std::filesystem features with examples, which will help us to highlight differences between C++11 and C++17 so we can get a better idea about what this new library will supply and how it might make developer's work easier.

std::filesystem::path

Upper we have seen a tiny use case for std::filesystem::path. That is a quite powerful and convenient feature that supplies an multi-platform abstraction for paths to files using the correct directory path separator depending on the platform we are building our application for (\ for Windows based systems and / Unix based systems).

Directory separator

When we want our application to use the correct directory separator in C++11, we could use conditional macro declaration:

#include <iostream>

using namespace std;

#ifdef _WIN32
const string SEP = "\\";
#else
const string SEP = "/";
#endif

int main()
{
    cout << "Separator in my system " << SEP << endl;
    return 0;
}

Compile and run: C++11 separator example

With C++17 it is just simpler:

#include <experimental/filesystem>
#include <iostream>

namespace fs = std::experimental::filesystem;
using namespace std;

int main()
{
    cout << "Separator in my system " << fs::path::preferred_separator << endl;
    return 0;
}

Compile and run: C++17 separator example

Directory Separator Operator

std::filesystem::path implements / operator which allows to easily concatenate paths to files and directories.

When we want to concatenate paths in C++11, we have to add extra logic to avoid adding duplicate separators and to select the correct separator for target platform:

#include <iostream>

using namespace std;

#ifdef _WIN32
const string SEP = "\\";
#else
const string SEP = "/";
#endif

int main()
{
    string root {"/"};
    string dir {"var/www/"};
    string index {"index.html"};

    string pathToIndex{};
    pathToIndex.append(root).append(SEP).append(dir).append(SEP).append(index);

    cout << pathToIndex << endl;
    return 0;
}

Compile and run: Concatenate paths in C++11.

Checking program output we notice it is not fully correct, we should have checked whether path parts already contains a separator so we don't append another separator again. That logic is already implemented in std::filesystem::path, so C++17 can be like:

#include <experimental/filesystem>
#include <iostream>

namespace fs = std::experimental::filesystem;
using namespace std;

int main()
{
    fs::path root {"/"};
    fs::path dir {"var/www/"};
    fs::path index {"index.html"};

    fs::path pathToIndex = root / dir / index;

    cout << pathToIndex << endl;
    return 0;
}

Compile and run: Concatenate paths in C++17. Code is cleaner and just correct, there are no duplicated separators.

Create/Remove Directories

std::filesystem comes with some utilities to create and remove files and directories, but firstly let's check out a way to do so in C++11.

#include <iostream>
#include <cstdio>
#include <sys/stat.h>

using namespace std;

int main()
{
    auto opts = S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH;
    mkdir("sandbox", opts);
    mkdir("sandbox/a", opts);
    mkdir("sandbox/a/b", opts);
    mkdir("sandbox/c", opts);
    mkdir("sandbox/c/d", opts);

    system("ls -la sandbox/*");

    remove("sandbox/c/d");
    remove("sandbox/a/b");
    remove("sandbox/c");
    remove("sandbox/a");
    remove("sandbox");

    system("ls -la");

    return 0;
}

Compile and run: Create and remove directories C++11. We have to create/remove one by one. We could rewrite this code snippet with less lines (using a loop), but we still have to pay attention to creation/deletion order, we cannot remove parent directory before we have removed all children.

Since C++17 we can create and remove nested directories with just one call.

#include <experimental/filesystem>
#include <iostream>

namespace fs = std::experimental::filesystem;
using namespace std;

int main()
{
    fs::create_directories("sandbox/a/b");
    fs::create_directories("sandbox/c/d");
    system("ls -la sandbox/*");

    cout << "Were directories removed? " << fs::remove_all("sandbox") << endl;
    system("ls -la");

    return 0;
}

Compile and run: Create and remove directories C++17.

Full example: Recursive Directory Iterator

This example consists of iterate recursively through dicrectories fintering files by extension.

To keep C++11 example simple, I haven't added filtering logic, but it is in C++17 example:

recursive-directory/filesystem.11.cpp (Source)

#include <dirent.h>
#include <cstring>
#include <iostream>
#include <fstream> // std::ofstream
#include <vector>
#include <memory>
#include <system_error>
#include <sys/stat.h>

using namespace std;

const string UP_DIR = "..";
const string CURRENT_DIR = ".";
const string SEP = "/";


string path(initializer_list<string> parts)
{
    string pathTmp {};
    string separator = "";
    for (auto & part: parts)
    {
        pathTmp.append(separator).append(part);
        separator = SEP;
    }
    return pathTmp;
}

vector<string> getDirectoryFiles(const string& dir, const vector<string> & extensions)
{
    vector<string> files;
    shared_ptr<DIR> directory_ptr(opendir(dir.c_str()), [](DIR* dir){ dir && closedir(dir); });
    if (!directory_ptr)
    {
        throw system_error(error_code(errno, system_category()), "Error opening : " + dir);
    }

    struct dirent *dirent_ptr;
    while ((dirent_ptr = readdir(directory_ptr.get())) != nullptr)
    {
        const string fileName {dirent_ptr->d_name};
        if (dirent_ptr->d_type == DT_DIR)
        {
            if (CURRENT_DIR != fileName && UP_DIR != fileName)
            {
                auto subFiles = getDirectoryFiles(path({dir, fileName}), extensions);
                files.insert(end(files), begin(subFiles), end(subFiles));
            }
        }
        else if (dirent_ptr->d_type == DT_REG)
        {
            // here we should check also if filename has an extension in extensions vector
            files.push_back(path({dir, fileName}));
        }
    }
    return files;
}

int main ()
{
    auto opt = S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH;
    mkdir("sandbox", opt);
    mkdir("sandbox/a", opt);
    mkdir("sandbox/a/b", opt);

        vector<string> e_files = {
            "./sandbox/a/b/file1.rst",
            "./sandbox/a/b/file1.txt",
            "./sandbox/a/file2.RST",
            "./sandbox/file3.md",
            "./sandbox/will_be.ignored"
        };

        // create files
        for (auto &f: e_files)
        {
                ofstream of(f, ofstream::out);
                of << "test";
        }

    cout << "filtered files: " << endl;
        for (auto &f: getDirectoryFiles(".", {".rst", ".RST", ".md"})){
            cout << "\t" << f << endl;
        }

    return 0;
}

Compile and run C++11 example.

Following example also filters files by extension.

recursive-directory/filesystem.17.cpp (Source)

#include <experimental/filesystem>
#include <iostream>
#include <vector>
#include <fstream>
#include <algorithm>    // std::find

namespace fs = std::experimental::filesystem;
using namespace std;

vector<string> getDirectoryFiles(const string & dir, const vector<string> & extensions)
{
        vector<string> files;
    for(auto & p: fs::recursive_directory_iterator(dir))
    {
        if (fs::is_regular_file(p))
        {
                if (extensions.empty() || find(extensions.begin(), extensions.end(), p.path().extension().string()) != extensions.end())
            {
                files.push_back(p.path().string());
            }
        }
    }
    return files;
}

int main()
{
    fs::create_directories("sandbox/a/b");
        vector<string> e_files = {
            "./sandbox/a/b/file1.rst",
            "./sandbox/a/b/file1.txt",
            "./sandbox/a/file2.RST",
            "./sandbox/file3.md",
            "./sandbox/will_be.ignored"
        };

        // create files
        for (auto &f: e_files)
        {
                ofstream(f) << "test";
        }

    cout << "filtered files: " << endl;
        for (auto &f: getDirectoryFiles(".", {".rst", ".RST", ".md"})){
            cout << "\t" << f << endl;
        }

    return 0;
}

Compile and run C++17 example.

Comments

Comments powered by Disqus