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

Popular posts from this blog

Graph Visualization using MSAGL with Examples

Step By Step Guide to Detect Heap Corruption in Windows Easily

How to Autogenerate Dump File using Windows Error Reporting : Easily Explained with Examples