C++17 的 std::filesystem 模块实战

在 C++17 中, 头文件正式加入标准库,为文件和目录的操作提供了统一且跨平台的接口。相比之前常用的 boost::filesystem 或系统特定的 API,std::filesystem 的优点在于标准化、简洁以及与 STL 的紧密结合。本文将通过一个完整的小项目来演示如何使用 std::filesystem 实现一个简易的“文件监视器”,并讨论其中的一些常见坑和优化技巧。

1. 环境准备

  • 编译器:g++ 9+ / clang++ 9+ / MSVC 2019+
  • C++17 标准开启:-std=c++17
  • 需要链接 stdc++fs(在某些旧版 GCC 上): -lstdc++fs

2. 需求概述

我们要实现一个命令行工具,监视指定目录下的文件变动(新增、删除、修改),并将变动记录到日志文件中。主要功能点:

  1. 目录遍历:递归读取子目录。
  2. 文件状态存储:使用 std::unordered_map<std::filesystem::path, std::filesystem::file_time_type> 存放文件路径及其最后修改时间。
  3. 变动检测:每隔一段时间(如 1 秒)重新遍历并比对上一次的状态。
  4. 日志输出:记录变动事件,格式为 [时间戳] 事件类型: 路径.

3. 核心代码实现

#include <filesystem>
#include <unordered_map>
#include <chrono>
#include <thread>
#include <iostream>
#include <fstream>
#include <iomanip>
#include <ctime>

namespace fs = std::filesystem;

// 用于将 file_time_type 转成可读时间字符串
std::string time_to_string(fs::file_time_type ft) {
    auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
        ft - fs::file_time_type::clock::now()
        + std::chrono::system_clock::now());
    std::time_t t = std::chrono::system_clock::to_time_t(sctp);
    std::tm tm;
#ifdef _WIN32
    localtime_s(&tm, &t);
#else
    localtime_r(&t, &tm);
#endif
    std::ostringstream oss;
    oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
    return oss.str();
}

// 记录日志
void log_event(const std::string& evt, const fs::path& p) {
    static std::ofstream ofs("file_monitor.log", std::ios::app);
    if (!ofs.is_open()) {
        std::cerr << "Cannot open log file!\n";
        return;
    }
    auto now = std::chrono::system_clock::now();
    std::time_t t = std::chrono::system_clock::to_time_t(now);
    std::tm tm;
#ifdef _WIN32
    localtime_s(&tm, &t);
#else
    localtime_r(&t, &tm);
#endif
    std::ostringstream oss;
    oss << "[" << std::put_time(&tm, "%Y-%m-%d %H:%M:%S") << "] " << evt << ": " << p << "\n";
    ofs << oss.str();
    ofs.flush();
}

// 递归遍历目录,更新状态映射
void scan_directory(const fs::path& root,
                    std::unordered_map<fs::path, fs::file_time_type>& state) {
    for (const auto& entry : fs::recursive_directory_iterator(root)) {
        if (entry.is_regular_file()) {
            state[entry.path()] = entry.last_write_time();
        }
    }
}

// 检测差异并记录
void detect_and_log(const std::unordered_map<fs::path, fs::file_time_type>& prev,
                    const std::unordered_map<fs::path, fs::file_time_type>& curr) {
    // 处理新增或修改
    for (const auto& [p, t] : curr) {
        auto it = prev.find(p);
        if (it == prev.end()) {
            log_event("新增", p);
        } else if (it->second != t) {
            log_event("修改", p);
        }
    }
    // 处理删除
    for (const auto& [p, _] : prev) {
        if (curr.find(p) == curr.end()) {
            log_event("删除", p);
        }
    }
}

int main(int argc, char* argv[]) {
    if (argc < 2) {
        std::cerr << "Usage: file_monitor <directory>\n";
        return 1;
    }
    fs::path dir(argv[1]);

    if (!fs::exists(dir) || !fs::is_directory(dir)) {
        std::cerr << "Invalid directory!\n";
        return 1;
    }

    std::unordered_map<fs::path, fs::file_time_type> prev;
    scan_directory(dir, prev);

    std::cout << "开始监视目录: " << dir << "\n";
    while (true) {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::unordered_map<fs::path, fs::file_time_type> curr;
        scan_directory(dir, curr);
        detect_and_log(prev, curr);
        prev.swap(curr);
    }
    return 0;
}

4. 常见坑与优化

场景 说明 解决办法
跨平台文件时间精度 Windows last_write_time 的精度为 100 ns,但在旧 Windows 版本中可能只有 1 ms 对时间进行比较时可以放宽误差阈值,或使用 std::chrono::duration_cast<std::chrono::milliseconds>
符号链接 recursive_directory_iterator 默认会遍历符号链接,可能导致无限递归 通过 fs::directory_options::follow_directory_symlink 或自行过滤 is_symlink()
文件删除后仍在迭代器中 删除后 last_write_time() 会抛异常 在遍历前捕获异常或检查 exists()
日志文件过大 持续写日志可能导致磁盘空间耗尽 添加日志轮转机制,或使用日志库如 spdlog

5. 进一步扩展

  • 事件过滤:只监视特定后缀的文件(.cpp, .h 等)。
  • 多线程:将扫描和日志写分离,使用 std::queue + 生产者/消费者模型。
  • 网络同步:将变动信息推送到远程服务器或通过 WebSocket 进行实时推送。
  • 事件回调:提供回调接口,让用户自行处理事件。

6. 总结

std::filesystem 的引入大幅简化了文件系统操作的代码量和可读性。通过以上示例,我们可以看到它在目录递归、文件属性查询、错误处理等方面都比传统的 Boost/OS 特定 API 更加直观。希望这篇实战文章能帮助你快速上手,并在自己的项目中合理利用 std::filesystem 的强大功能。

发表评论