Comparison Story: DirNav

3.  DirNav — callback-driven depth-first walker

3.  DirNav

DirNav is a depth-first directory walker driven by two std::function callbacks — one fired on entering a directory, one on encountering a matching file:
using DirCallback  = std::function<void(const std::string& dir_path)>;
using FileCallback = std::function<void(const std::string& file_name)>;
Where the Rust variant expresses the callback contract as a trait implemented by an application-specific struct, the C++ variant uses two std::function members that any caller can populate with a lambda, free function, or method pointer. This trade-off removes the type-parameter plumbing but pays a small runtime cost per call (indirect call through the type-erased wrapper).

3.1  Design Points

  • Skip list is baked in. Build directories (build, out, bin, obj, target), Python caches, VCS/IDE folders, and archive are pre-loaded into skip_list_ in the constructor initializer list so no callers have to know the list. Extra names can be added via add_skip().
  • Extensions normalised at insert. normalise_ext() strips a leading . and, on Windows, lowercases the string — done once when the pattern is registered and again on each file's extension. This makes patterns_.contains(...) a straight O(1) hash lookup on the hot path with no per-lookup allocation.
  • Subdirectories are collected, then recursed. The scan pass fires file_callback_ immediately for each matching file, but subdirectories are buffered in a std::vector and processed only after the current directory is fully drained. This keeps the directory_iterator in one linear pass — no interleaved recursion inside its loop.
  • Errors swallowed via std::error_code. Every std::filesystem call is the non-throwing overload; ec parameters are checked but errors do not abort traversal — a permission-denied subdirectory simply drops out and the walk continues.
  • Counts reset on each visit(). file_count_ and dir_count_ are zeroed at the top of every top-level visit(), so the same DirNav instance can drive multiple sequential walks.
  • Directory fires unconditionally. Unlike the Rust walker, DirNav fires dir_callback_ every time it enters a directory — the "hide empty" logic lives in Output, which defers printing the directory header until it sees a matching file.

3.2  visit_impl() — the traversal loop

  1. Increment dir_count_ and fire dir_callback_ with the directory's generic_string().
  2. Open a directory_iterator; on error, return.
  3. For each entry: if it is a regular file whose extension matches, bump file_count_ and fire file_callback_. If it is a directory whose bare name is not in the skip list and recursion is enabled, push its path onto the subdirs vector.
  4. After the scan pass, recurse into each buffered subdirectory.

3.3  Source — DirNav/src/DirNav.ixx

export module dir_nav;

import std;

export {

using DirCallback  = std::function<void(const std::string& dir_path)>;
using FileCallback = std::function<void(const std::string& file_name)>;

class DirNav {
public:
    explicit DirNav(bool recurse = true)
        : recurse_(recurse)
        , skip_list_({
              // C#/.NET
              "bin", "obj",
              // Rust
              "target",
              // C++
              "build", "out",
              // Python
              "__pycache__", ".venv", "venv", "dist",
              // VCS / IDE
              ".git", ".vs", ".idea",
              // archives
              "archive"
          })
        , file_count_(0)
        , dir_count_(0)
    {}

    void set_dir_handler(DirCallback cb) {
        dir_callback_ = std::move(cb);
    }

    void set_file_handler(FileCallback cb) {
        file_callback_ = std::move(cb);
    }

    void add_pattern(const std::string& ext) {
        patterns_.insert(normalise_ext(ext));   // normalise once at insert
    }

    void add_skip(const std::string& name) {
        skip_list_.insert(name);
    }

    bool visit(const std::filesystem::path& root) {
        std::error_code ec;
        bool exists = std::filesystem::exists(root, ec);
        if (ec || !exists) return false;
        bool is_dir = std::filesystem::is_directory(root, ec);
        if (ec || !is_dir) return false;

        file_count_ = 0;
        dir_count_  = 0;

        visit_impl(root);
        return true;
    }

    std::size_t file_count() const { return file_count_; }
    std::size_t dir_count()  const { return dir_count_;  }

private:
    bool                            recurse_;
    std::unordered_set<std::string> skip_list_;
    std::unordered_set<std::string> patterns_;
    DirCallback                     dir_callback_;
    FileCallback                    file_callback_;
    std::size_t                     file_count_;
    std::size_t                     dir_count_;

    // Strip leading dot and lowercase on Windows so lookups are O(1) and allocation-free.
    static std::string normalise_ext(std::string s) {
        if (!s.empty() && s.front() == '.') s.erase(s.begin());
#if defined(_WIN32)
        std::transform(s.begin(), s.end(), s.begin(),
                       [](unsigned char c){ return static_cast<char>(std::tolower(c)); });
#endif
        return s;
    }

    bool extension_matches(const std::filesystem::path& file_path) const {
        if (patterns_.empty()) return true;
        return patterns_.contains(normalise_ext(file_path.extension().string()));
    }

    void visit_impl(const std::filesystem::path& dir) {
        ++dir_count_;

        if (dir_callback_)
            dir_callback_(dir.generic_string());

        std::error_code ec;
        std::filesystem::directory_iterator it(dir, ec);
        if (ec) return;

        std::vector<std::filesystem::path> subdirs;
        for (const auto& entry : it) {
            std::error_code entry_ec;

            bool is_regular = entry.is_regular_file(entry_ec);
            if (!entry_ec && is_regular) {
                if (extension_matches(entry.path())) {
                    ++file_count_;
                    if (file_callback_)
                        file_callback_(entry.path().filename().generic_string());
                }
                continue;
            }

            bool is_directory = entry.is_directory(entry_ec);
            if (!entry_ec && is_directory) {
                std::string bare_name = entry.path().filename().string();
                if (!skip_list_.contains(bare_name) && recurse_)
                    subdirs.push_back(entry.path());
            }
        }

        for (const auto& subdir : subdirs)
            visit_impl(subdir);
    }
};

} // export