先把时间拨回去
那时候还没有 std::filesystem。
“路径”就是字符串。
你能拼出来。
就算完成任务。
"dir/" + name 这种写法很常见。
第一眼没毛病。
第二眼也没毛病。
直到你把程序搬到另一台机器。
Linux 用 /。
Windows 常见 \。
你开始写 #ifdef _WIN32。
你以为你在做兼容。
其实你在攒技术债。
我当年是怎么把坑越补越大的
我第一反应是补分隔符。目录末尾没有 / 就补一个,有两个 / 就删一个。
这招能救一半场景,但救不了语义。这里的“语义”就是:这个值在程序里到底代表什么。
字符串只知道自己长什么样,不知道自己是不是一条合规路径。
我后来又补了第二层:把 \\ 都替换成 /。本地看起来更整齐了,线上却更乱。
Windows 的盘符路径和 UNC 网络路径(像 \\\\server\\share 这种“远程共享目录”)并不按这个规则走。
那一刻我才明白。问题不在“字符长得丑”,问题在“路径被当成了普通文本”。
事故现场:日志没轮转,磁盘写满
周五晚上。
报警先响。
然后服务重启。
再然后同事问:日志怎么跑到两个目录里了?
我打开配置。
只改了一行路径。
最小复现:字符串拼接会把路径拼歪
#include <string>
std::string log_file(std::string dir, std::string name) {
if (!dir.empty() && dir.back() != '/')
dir += '/';
return dir + name;
}
这段代码在 Linux 常常“看着能用”。
但当 dir 来自 Windows 配置,比如 C:\\logs,结果会变成 C:\\logs/app.log。
有些 API 会接受它,有些工具会把它当成另一条路径,问题就开始随机出现。
真正缺的不是补丁,而是“路径类型”
我后来换了一个思路:别让函数接收“任意字符串”,它应该接收“专门表示路径”的类型。
C++17 里,这个类型就是 std::filesystem::path。你可以把它理解成“懂路径规则的字符串容器”。
path 不是魔法,但它知道分隔符、根目录、文件名这些结构。
这里的 / 也不是除法,而是“拼路径段”。
同一段逻辑,用 path 写会稳很多
#include <filesystem>
namespace fs = std::filesystem;
fs::path log_file(fs::path dir, fs::path name) {
return dir / name;
}
这段代码短,关键是少猜。
你不再手写“该不该补 /”,而是让标准库按平台规则处理。
代码可读性也更直接:这不是字符串拼接,这是路径组合。
另一个常见坑:先 exists 再 create 的竞态
#include <filesystem>
namespace fs = std::filesystem;
if (!fs::exists("logs")) {
fs::create_directories("logs");
}
这写法很常见,也很危险。
“先检查再操作”中间如果有别的线程或进程插进来,你就可能撞上竞态条件。
竞态条件可以简单理解成:你刚看完世界,世界就变了。
更稳的写法:直接创建,并接住错误码
#include <filesystem>
#include <system_error>
namespace fs = std::filesystem;
std::error_code ec;
fs::create_directories("logs", ec);
if (ec) {
// 这里只处理真正的失败
}
std::error_code 是“错误码对象”,你可以把它当成 errno 的 C++ 版本。
它的好处是可控:不靠异常跳流程,你可以在每一步显式处理失败。
线上排查时,这种写法通常更容易追到根因。
关键结论
std::filesystem 不是“多几个文件 API”。
它是把“路径”从文本升级成类型。
一旦类型对了。
很多 bug 在编译期和评审期就已经没机会长大。
收尾给你一个反直觉的小观察
很多人以为 filesystem 解决的是“跨平台”。
其实它先解决的是“同一个平台里,大家对路径的理解不一致”。
先把语义收拢。
再谈跨平台。
这顺序一换,代码世界会安静很多。