C++17中std::filesystem的实际应用案例

在C++17中引入的std::filesystem库为文件系统操作提供了统一且现代化的接口,极大地简化了文件和目录的处理。本文通过一个完整的实战案例,展示如何利用std::filesystem完成日志文件的滚动、文件夹扫描以及跨平台路径处理,并在代码中加入异常安全与性能优化的细节。

1. 需求分析

假设我们正在开发一款需要持续写入日志的服务器程序。每写入一个日志文件超过100MB,系统会自动切分成新的文件,保留最近N个日志文件,过旧的文件则被删除。与此同时,程序还需要能扫描日志目录,生成一个包含所有日志文件大小的报告,支持Windows、Linux和macOS三大主流平台。

2. 关键技术点

技术 说明 关键代码
路径操作 使用std::filesystem::path自动处理不同系统下的路径分隔符。 auto p = std::filesystem::path{log_dir} / "log.txt";
文件大小检测 std::filesystem::file_size获取文件字节数。 auto sz = std::filesystem::file_size(p);
目录遍历 std::filesystem::recursive_directory_iterator递归遍历目录。 for (auto const & entry : std::filesystem::recursive_directory_iterator(dir))
异常安全 所有filesystem函数抛异常时使用try-catch捕获,并记录错误。 try { ... } catch(const std::filesystem::filesystem_error& e) {}
性能优化 采用std::filesystem::directory_iterator而不是递归迭代器,除非需要递归;使用std::filesystem::space获取磁盘剩余空间。 auto space = std::filesystem::space(dir);

3. 代码实现

下面的代码片段展示了完整的日志滚动与报告生成逻辑。为了便于阅读,已删除了不必要的头文件与宏定义,直接列出核心实现。

#include <filesystem>
#include <fstream>
#include <iostream>
#include <chrono>
#include <iomanip>
#include <sstream>

namespace fs = std::filesystem;

// 生成唯一的日志文件名,例如 log_20240113_142500.txt
std::string generate_log_name(const fs::path& dir)
{
    auto now = std::chrono::system_clock::now();
    auto time = std::chrono::system_clock::to_time_t(now);
    std::tm tm;
#if defined(_WIN32) || defined(_WIN64)
    localtime_s(&tm, &time);
#else
    localtime_r(&time, &tm);
#endif
    std::ostringstream oss;
    oss << "log_" << std::put_time(&tm, "%Y%m%d_%H%M%S") << ".txt";
    return (dir / oss.str()).string();
}

// 关闭当前日志文件并创建新文件
void rollover_log(std::ofstream& ofs, const fs::path& dir, std::string& current_name)
{
    ofs.close();
    current_name = generate_log_name(dir);
    ofs.open(current_name, std::ios::out | std::ios::app);
}

// 只保留最近N个日志文件
void prune_old_logs(const fs::path& dir, std::size_t keep = 5)
{
    std::vector<fs::directory_entry> logs;
    for (auto const& entry : fs::directory_iterator(dir))
    {
        if (entry.is_regular_file() && entry.path().extension() == ".txt")
            logs.push_back(entry);
    }

    // 按修改时间排序,旧的在前
    std::sort(logs.begin(), logs.end(),
        [](auto const& a, auto const& b) {
            return fs::last_write_time(a) < fs::last_write_time(b);
        });

    if (logs.size() <= keep) return;

    for (std::size_t i = 0; i < logs.size() - keep; ++i)
    {
        try { fs::remove(logs[i].path()); }
        catch (const fs::filesystem_error& e) { std::cerr << e.what() << '\n'; }
    }
}

// 生成日志目录报告
void generate_report(const fs::path& dir, const fs::path& report_file)
{
    std::ofstream rpt(report_file, std::ios::out);
    rpt << "Log Report - " << std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()) << '\n';
    rpt << "---------------------------------------------------\n";
    std::uintmax_t total_size = 0;
    for (auto const& entry : fs::directory_iterator(dir))
    {
        if (!entry.is_regular_file() || entry.path().extension() != ".txt") continue;
        auto sz = fs::file_size(entry.path());
        rpt << std::setw(40) << std::left << entry.path().filename().string() << std::right << std::setw(12) << sz << " bytes\n";
        total_size += sz;
    }
    rpt << "---------------------------------------------------\n";
    rpt << std::setw(40) << "Total Size:" << std::right << std::setw(12) << total_size << " bytes\n";
}

// 主程序入口示例
int main()
{
    const fs::path log_dir = "./logs";
    try
    {
        if (!fs::exists(log_dir)) fs::create_directories(log_dir);
    }
    catch (const fs::filesystem_error& e)
    {
        std::cerr << "创建日志目录失败: " << e.what() << '\n';
        return 1;
    }

    std::string current_log = generate_log_name(log_dir);
    std::ofstream ofs(current_log, std::ios::out | std::ios::app);
    if (!ofs.is_open())
    {
        std::cerr << "打开日志文件失败\n";
        return 1;
    }

    // 简化示例:每秒写入一行,超过100MB时滚动
    const std::uintmax_t MAX_SIZE = 100 * 1024 * 1024; // 100 MB
    for (int i = 0; ; ++i)
    {
        ofs << "Log entry #" << i << " at " << std::chrono::system_clock::now().time_since_epoch().count() << '\n';
        if (ofs.tellp() >= static_cast<std::streamoff>(MAX_SIZE))
            rollover_log(ofs, log_dir, current_log);

        std::this_thread::sleep_for(std::chrono::seconds(1));

        // 每5分钟执行一次日志清理和报告生成
        if (i % 300 == 0)
        {
            prune_old_logs(log_dir, 5);
            generate_report(log_dir, log_dir / "report.txt");
        }
    }
    return 0;
}

代码说明

  1. 路径统一
    fs::path 把 Windows 的 \\ 与 POSIX 的 / 自动转义,保证跨平台兼容。

  2. 异常处理
    所有文件系统操作都被包裹在 try-catch 里,防止因磁盘错误导致程序崩溃。

  3. 性能注意

    • 使用 fs::directory_iterator 而非递归迭代器,因为日志目录一般不需要递归。
    • 只在必要时读取文件大小,避免无谓的 I/O。
  4. 可扩展性

    • 可以通过配置文件调整 MAX_SIZE、保留文件数量等参数。
    • 报告文件支持输出为 CSV 或 JSON,方便后续分析。

4. 总结

  • std::filesystem 用起来几乎无需额外库,标准化的接口为文件系统操作带来了极大的便利。
  • 正确的路径拼接、异常安全与性能优化是编写健壮跨平台代码的三大核心。
  • 在C++17及之后的标准中,std::filesystem 已成为处理文件的首选工具,任何涉及文件读写的项目都值得优先考虑使用。

通过上述案例,读者可以快速上手 std::filesystem,并将其应用于日志管理、配置文件解析、资源预加载等多种实际场景。祝编码愉快!

发表评论