std::filesystem¶
| 标准 | 难度 | 状态 | 分级 | 证据 | 审阅 | 最后校验 |
|---|---|---|---|---|---|---|
| C++17 | 3/5 | final | A | 3 条(manual 3) | 单审(zyuhao7) | 2026-10-24 |
编译器支持:GCC 8+ · Clang 7+ · MSVC 19.14+。版本查询:cppreference 编译器支持。
主张与证据¶
| 主张 | 支持证据 |
|---|---|
| 为 std::filesystem::path 特化 formatter | ev-fmt-filesystem-path-formatter-002 |
| 从 path 取出指定字符类型的原生字符串 | ev-fmt-filesystem-path-helper-001 |
| 用命名空间别名统一标准与实验版 filesystem | ev-json-filesystem-alias-003 |
本站证据页:ev-fmt-filesystem-path-formatter-002 · ev-fmt-filesystem-path-helper-001 · ev-json-filesystem-alias-003
解决什么问题¶
C++17 之前,跨平台文件操作要分叉:POSIX 用 stat、opendir、unlink,Windows 用 CreateFile、FindFirstFile。路径拼接靠手写分隔符,编码在 char 与 wchar_t 之间来回转换,错误处理混用 errno 和 GetLastError。Boost.Filesystem 能解决,但引入整库依赖。
std::filesystem 把路径表示、属性查询、目录遍历和文件操作收进标准库,并提供统一的 error_code / 异常两条错误通道。它是库设施,不改变语言规则;核心价值是消除平台分支与路径字符串拼装错误。
基础用法¶
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path p = "/var/log/app/server.log";
std::cout << p.parent_path().string() << '\n';
std::cout << p.stem().string() << '\n';
std::cout << p.extension().string() << '\n';
std::cout << (p.parent_path() / "backup.log").generic_string() << '\n';
}
路径分解是纯词法操作,不访问磁盘;后半段用临时目录演示创建、遍历与属性查询。完整示例见 examples/cpp17/filesystem.cpp:
/var/log/app
server .log
/var/log/app/backup.log
entries: archive config.ini server.log
size: 5
relative: logs/server.log
进阶用法¶
词法操作与文件系统操作要分开理解。 parent_path、filename、lexically_normal 只处理字符串;exists、canonical、file_size 会触碰磁盘。canonical 要求路径存在,weakly_canonical 允许末尾不存在,适合“准备创建新文件”的场景。
优先使用 error_code 重载。 每个会失败的函数都有 (..., std::error_code&) 版本。批量扫描目录时,异常版本会在单个权限错误处中断;error_code 版本可以记录后继续:
std::error_code ec;
fs::directory_iterator it(dir, ec), end;
if (ec) return; // 构造失败,例如目录不存在或无权限
for (; it != end; it.increment(ec)) {
if (ec) break; // 自增失败,例如遍历中目录被删除
const auto& entry = *it;
// ...
}
构造函数和 increment 都接收 error_code,所以单个无权限目录不会中断整个遍历;但构造失败时迭代器直接等于 end,必须在循环前检查 ec。范围 for 版本只在构造时能传 ec,自增失败仍会抛异常;需要完全无异常时使用上面的显式循环。
区分 status 与 symlink_status。 status 跟随符号链接,symlink_status 返回链接自身。删除链接用 remove 只删链接;遍历时想跳过链接目录,需先判断 is_symlink。
路径字符串的编码随平台变化。 string() 使用原生窄字符表示,Windows 上可能抛 filesystem_error;generic_string() 统一用 /;u8string() 在 C++17 返回 std::string,C++20 起返回 std::u8string,这是源码级不兼容点。需要稳定 UTF-8 时显式用 generic_u8string() 并固定标准版本。
目录遍历有递归与非递归两类。 recursive_directory_iterator 支持 disable_recursion_pending() 剪枝、skip_permission_denied 跳过无权限目录。遍历顺序未指定,不要依赖出现顺序。
文件大小与时间不是原子快照。 file_size、last_write_time 是两次独立查询,文件可能在查询之间被修改。需要一致性时自己打开文件并读取元数据。
真实项目证据¶
| 项目 | 位置 | 用法 | 为什么典型 |
|---|---|---|---|
| fmtlib/fmt | include/fmt/std.h#L102-L111 |
从 filesystem::path 取出指定字符类型的原生字符串 |
把 path 当作可查询对象,按目标字符类型选择转换路径 |
| fmtlib/fmt | include/fmt/std.h#L305-L313 |
为 std::filesystem::path 提供格式化器 |
库边界上适配标准类型,而不是要求用户先转 string |
| nlohmann/json | include/nlohmann/detail/meta/std_fs.hpp#L21-L28 |
用命名空间别名统一 std::filesystem 与实验版 |
兼容旧标准库时集中隔离差异,业务代码只认 std_fs |
三条证据均为 manual 级:作者复核,尚未引入第二名审核者。
陷阱与边界¶
canonical会失败于不存在路径。 构造目标路径时用weakly_canonical,或先absolute再lexically_normal。exists之后再打开存在 TOCTOU 窗口。 文件可能在两次调用之间被删除或替换;安全检查不能只靠exists。remove不递归。 非空目录会失败;删目录树用remove_all,并注意它会删除符号链接本身而非目标。- 遍历时删除元素会破坏迭代器。 需要删除时先收集路径,遍历结束后统一处理。
- 路径比较是词法的。
path("a/../b") != path("b");大小写敏感性由文件系统决定,标准库不做规范化。 - 隐式转换不存在。
path不会自动转成std::string;需要显式.string()或.generic_string()。 - 流输出会给路径加引号。
std::cout << p按引用形式输出(例如"/var/log/app"),与.string()的输出不同;日志格式需要稳定文本时显式转换。 u8string()的返回类型在 C++20 变了。 跨标准编译的代码不要写auto s = p.u8string();再按std::string使用。- 权限位不等于访问控制。
permissions()映射到 POSIX 位,Windows ACL 无法完整表达;不要把它当作安全边界。
库联动¶
- fmt:
fmt/std.h直接为std::filesystem::path提供格式化器,支持{}、{:g}(generic 分隔符)和调试转义。好处是日志里不再手动.string();代价是引入fmt/std.h后所有path格式化都走该实现,Windows 宽字符路径会触发 UTF-8 转换。 - nlohmann/json:通过
detail::std_fs别名兼容std::filesystem与std::experimental::filesystem,读写路径字段时不必关心底层标准库版本。 - spdlog:日志消息常直接拼接路径;结合
fmt的path格式化器可避免临时std::string,但路径中含非 UTF-8 字节时仍要自行处理编码。
横向对比¶
| 方案 | 可移植性 | 错误模型 | 主要代价 |
|---|---|---|---|
std::filesystem |
标准库覆盖的平台 | 异常或 error_code |
实现质量随标准库版本变化;路径编码差异仍在 |
| POSIX API | 类 Unix | errno |
Windows 需要另一套实现 |
| Win32 API | Windows | GetLastError |
不可移植,宽字符路径更自然 |
| Boost.Filesystem | 广泛 | 异常或 error_code |
额外依赖;与标准版行为有细微差异 |
C FILE* / fopen |
广泛 | 返回值 + errno |
只处理文件内容,不覆盖目录与属性 |
跨标准演进:C++17 引入 std::filesystem;C++20 改变 u8string() 的返回类型;后续标准未再改动核心接口。C++26 的文件系统扩展仍以库提案形式讨论,使用前应确认目标标准库实现。