RAII 与 unique_ptr 的自定义删除器
RAII 的核心很简单:资源的生命周期绑定到对象的生命周期上,构造时获取,析构时释放。标准库里的 std::unique_ptr 默认用 delete 释放,但它的第二个模板参数允许换成任意删除器,所以也能用来管理不是 new 出来的资源。
管理 FILE*
最常见的写法是传一个函数指针:
#include <cstdio>
#include <memory>
using FilePtr = std::unique_ptr<FILE, int(*)(FILE*)>;
FilePtr open_file(const char* path) {
return FilePtr(std::fopen(path, "r"), &std::fclose);
}
能用,但有一个代价:删除器是函数指针,unique_ptr 必须把它存下来,对象大小从 8 字节变成 16 字节。
用空类型删除器省掉这 8 字节
把删除器写成无状态的类型,标准库实现会利用空基类优化把它压掉:
struct FileCloser {
void operator()(FILE* f) const noexcept {
if (f) std::fclose(f);
}
};
using FilePtr = std::unique_ptr<FILE, FileCloser>;
static_assert(sizeof(FilePtr) == sizeof(FILE*));
C++20 之后还可以直接用 lambda 的类型,因为无捕获 lambda 可以默认构造了:
using FilePtr = std::unique_ptr<FILE, decltype([](FILE* f) { std::fclose(f); })>;
不过这种写法放在头文件里要小心,每个翻译单元里的 lambda 是不同的类型,容易踩到 ODR 的问题。我一般还是老老实实写一个具名结构体。
管理文件描述符
int 类型的 fd 不是指针,不能直接塞进 unique_ptr。这种情况自己写一个小类更清楚:
class UniqueFd {
public:
explicit UniqueFd(int fd = -1) noexcept : fd_(fd) {}
~UniqueFd() { reset(); }
UniqueFd(UniqueFd&& o) noexcept : fd_(std::exchange(o.fd_, -1)) {}
UniqueFd& operator=(UniqueFd&& o) noexcept {
if (this != &o) reset(std::exchange(o.fd_, -1));
return *this;
}
UniqueFd(const UniqueFd&) = delete;
UniqueFd& operator=(const UniqueFd&) = delete;
int get() const noexcept { return fd_; }
void reset(int fd = -1) noexcept {
if (fd_ >= 0) ::close(fd_);
fd_ = fd;
}
private:
int fd_;
};
小结
- 删除器优先写成空结构体,不占额外空间
- 非指针句柄(fd、Windows 的 HANDLE 等)单独封装一个只能移动的类
- 析构函数和删除器都应该是
noexcept的