oop::notes

RAII 与 unique_ptr 的自定义删除器

2026-05-11 · #cpp#memory

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_;
};

小结