不能直接用std::fstream因其不暴露fd且带缓冲;FileDescriptor用RAII封装open/close,explicit构造、禁拷贝、支持移动,析构时安全close,open必带O_CLOEXEC。

为什么不能直接用 std::fstream 就完事?
因为 std::fstream 不暴露底层文件描述符(int fd),没法和系统调用(如 epoll_ctl、sendfile、splice)配合;而且它默认带缓冲,行为不可控。RAII 文件句柄类要做的,是把 open/close 这对系统调用封装成构造/析构的自动管理,同时保证异常安全、移动语义正确、不隐式转换。
FileDescriptor 的核心成员和构造逻辑
只存一个 int fd = -1,构造时调用 open(),失败则抛 std::system_error(用 errno 构造);析构时仅当 fd != -1 才调用 close(fd),并忽略 close 返回值(POSIX 允许且常见)。不要在构造函数里做多余检查,比如验证路径是否存在——那是调用者的责任。
关键点:
- 构造函数必须是
explicit,防止int隐式转成对象 - 禁止拷贝:删掉拷贝构造和拷贝赋值(
= delete) - 支持移动:实现移动构造和移动赋值,移动后源对象的
fd必须置为-1 - 提供
get()成员函数返回fd,不提供隐式转换操作符
移动赋值中容易漏掉的 close() 调用
移动赋值不是“把资源拿走就完事”,原对象可能持有有效 fd,必须先清理再接管新资源。典型错误写法是直接赋值 fd = other.fd; other.fd = -1;,忘了关掉当前已有的句柄。
立即学习“C++免费学习笔记(深入)”;
正确写法示例片段:
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
FileDescriptor& operator=(FileDescriptor&& other) noexcept {
if (this != &other) {
if (fd != -1) close(fd); // ← 这行常被跳过
fd = other.fd;
other.fd = -1;
}
return *this;
}注意:close() 可能失败(比如 EBADF),但此时已无法可靠报告,只能忽略;加日志或断言调试时可检查返回值,但生产代码里不应 throw。
使用 O_CLOEXEC 是默认安全底线
Linux/macOS 下,open() 必须带上 O_CLOEXEC 标志,否则 fork() + exec() 后子进程会意外继承该 fd,造成资源泄露或安全风险。Windows 没有对应机制,但用 _open() 时应传 _O_NOINHERIT。
示例构造:
explicit FileDescriptor(const char* path, int flags = O_RDONLY | O_CLOEXEC)
: fd(open(path, flags)) {
if (fd == -1) {
throw std::system_error(errno, std::generic_category(), path);
}
}别依赖 fcntl(fd, F_SETFD, FD_CLOEXEC) 补救——竞态窗口存在,且多线程下不安全。
真正难的是错误传播粒度:要不要区分 open 失败是因为权限不足(EACCES)、路径不存在(ENOENT)还是磁盘满(ENOSPC)?RAII 类本身不该做业务判断,只负责“打开失败就抛”,让上层决定怎么处理。细节藏在 std::system_error 的 code().value() 里,够用就行。

















