The Ultimate Guide to C++17 std::filesystem: List, Filter, and Manage Files

If you have been coding in C++ for more than a few years, you likely remember the "Dark Ages" of file manipulation. If you wanted to list the files in a directory, you had to write one implementation for Windows (using <windows.h>), another for Linux/macOS (using <dirent.h>), and then wrap it all in ugly preprocessor directives.

It was messy, error-prone, and difficult to maintain.

Enter C++17 and the std::filesystem library.

This library is arguably one of the most significant quality-of-life improvements in the modern C++ standard. It provides a robust, cross-platform way to interact with the file system, manipulate paths, and iterate over directories.

In this ultimate guide, we won't just list files; we will build a complete understanding of how std::filesystem works. By the end of this tutorial, you will know how to:

  • Set up your environment for C++17.
  • Iterate through directories efficiently.
  • Perform recursive searches (scanning subfolders).
  • Filter files by extension (e.g., finding all .jpg files).
  • Handle errors gracefully without crashing your application.
  • Build a real-world "Disk Analyzer" tool.

Let’s simplify your day by simplifying your code.


1. Setting the Stage: Environment Setup

Before we write a single line of code, we need to ensure your compiler is ready. std::filesystem was officially added in the C++17 standard.

Compiler Flags

To use the library, you must tell your compiler to use the C++17 standard.

  • GCC (g++) and Clang: Add the -std=c++17 flag.
    g++ main.cpp -o app -std=c++17
  • Visual Studio (MSVC):
    1. Right-click your Project in the Solution Explorer.
    2. Select Properties.
    3. Go to Configuration Properties > C/C++ > Language.
    4. Set "C++ Language Standard" to ISO C++17 Standard (/std:c++17).

The Header

The header file is simply:

#include <filesystem>

// Standard alias to save typing
namespace fs = std::filesystem;

2. The Core Concept: fs::path

Understanding the fs::path object is crucial. In the old days, we stored file paths as strings. This was problematic because Windows uses backslashes (\) while Linux uses forward slashes (/).

The fs::path class abstracts this away. It represents a path on the filesystem regardless of the operating system.

fs::path myPath = "photos";
myPath /= "vacation"; // Automatically adds / or \ depending on OS
myPath /= "beach.jpg";

std::cout << myPath << std::endl; 
// Output on Windows: "photos\vacation\beach.jpg"
// Output on Linux:   "photos/vacation/beach.jpg"

Key Takeaway: Whenever you are working with filenames, use fs::path objects instead of raw strings.


3. Basic Directory Iteration

Let’s tackle the primary task: listing files in a folder. We use fs::directory_iterator, which is highly efficient.

Here is the robust way to list files:

#include <iostream>
#include <filesystem>
#include <string>

namespace fs = std::filesystem;

void listFiles(const std::string& pathString) {
    fs::path path(pathString);

    // 1. Check if path exists and is actually a directory
    if (!fs::exists(path) || !fs::is_directory(path)) {
        std::cerr << "Error: Path does not exist or is not a directory." << std::endl;
        return;
    }

    std::cout << "Scanning directory: " << fs::absolute(path) << "\n\n";

    // 2. Iterate
    for (const auto& entry : fs::directory_iterator(path)) {
        std::cout << "Found: " << entry.path().filename();
        
        if (entry.is_directory()) {
            std::cout << " [DIR]";
        } else if (entry.is_regular_file()) {
             std::cout << " (" << entry.file_size() << " bytes)";
        }
        std::cout << std::endl;
    }
}

int main() {
    listFiles("."); // Scan current directory
    return 0;
}

4. Going Deeper: Recursive Directory Iteration

A standard iterator is "shallow"—it doesn't look inside subfolders. To look inside subfolders (and sub-subfolders), we use fs::recursive_directory_iterator.

void scanRecursively(const fs::path& path) {
    std::cout << "Recursive scan of: " << path << "\n";
    
    try {
        for (const auto& entry : fs::recursive_directory_iterator(path)) {
            // Indent based on depth for visual clarity
            int depth = entry.depth(); 
            std::string indent(depth * 2, ' '); 

            std::cout << indent << "- " << entry.path().filename() << "\n";
        }
    } catch (const fs::filesystem_error& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }
}

5. Advanced Filtering: Finding Specific File Types

Usually, we are looking for something specific, like all .csv reports. We can inspect the extension of the path efficiently.

#include <vector>

std::vector<fs::path> findFilesByExtension(const fs::path& root, const std::string& ext) {
    std::vector<fs::path> matches;

    if (!fs::exists(root) || !fs::is_directory(root)) return matches;

    for (const auto& entry : fs::recursive_directory_iterator(root)) {
        if (entry.is_regular_file()) {
            if (entry.path().extension() == ext) {
                matches.push_back(entry.path());
            }
        }
    }
    return matches;
}

6. Project: Building a "Disk Usage Analyzer"

Let's put everything together. We will create a simple tool that scans a folder recursively and calculates the total size of all files inside it.

#include <iostream>
#include <filesystem>
#include <numeric>

namespace fs = std::filesystem;

// Helper to format bytes into readable KB/MB
std::string formatSize(uintmax_t bytes) {
    if (bytes >= 1024 * 1024 * 1024) {
        return std::to_string(bytes / (1024 * 1024 * 1024)) + " GB";
    } else if (bytes >= 1024 * 1024) {
        return std::to_string(bytes / (1024 * 1024)) + " MB";
    } else if (bytes >= 1024) {
        return std::to_string(bytes / 1024) + " KB";
    }
    return std::to_string(bytes) + " bytes";
}

int main() {
    std::string inputPath;
    std::cout << "Enter directory path to analyze: ";
    std::getline(std::cin, inputPath);

    fs::path rootPath(inputPath);
    uintmax_t totalSize = 0;
    size_t fileCount = 0;

    if (fs::exists(rootPath) && fs::is_directory(rootPath)) {
        std::cout << "Analyzing... (this may take a moment)\n";

        try {
            for (const auto& entry : fs::recursive_directory_iterator(rootPath)) {
                if (fs::is_regular_file(entry)) {
                    totalSize += entry.file_size();
                    fileCount++;
                }
            }
        } catch (const fs::filesystem_error& e) {
            std::cerr << "Stopped execution due to error: " << e.what() << "\n";
        }

        std::cout << "Total Files: " << fileCount << "\n";
        std::cout << "Total Size:  " << formatSize(totalSize) << "\n";

    } else {
        std::cout << "Invalid directory path.\n";
    }

    return 0;
}

Summary

The std::filesystem library abstracts away the messy details of OS-specific file systems, allowing you to write clean, portable, and readable code.

Recap of Best Practices:

  • Always enable C++17 or newer.
  • Use fs::path instead of string.
  • Use recursive_directory_iterator for deep scans.
  • Wrap filesystem operations in try-catch blocks.

Have you used std::filesystem in your projects yet? Let me know in the comments below!

Comments