Item 13:以对象管理资源

对齐 Effective C++ 第三版 Item 13:资源获取后立即交给 RAII 对象,以 destructor 覆盖正常和异常退出,并选择 value、unique、shared、weak 或 custom ownership。

学习目标

  • 能复现手工 new/delete、open/close 与 lock/unlock 在 early return 和 exception 下的泄漏路径
  • 能实现 acquisition-is-initialization 的 RAII owner,使资源获取后立即受对象管理并在所有退出路径释放一次
  • 能比较 value、unique、shared、weak 与 custom-deleter ownership,设计 factory 和失败注入测试
Acquire / own / release所有权在资源成功后立即进入对象生命周期acquireraw handle / lock失败前不可泄漏objects to manageresourcesimmediate ownerraiidestructor cleanupnormal / exceptionsmart pointerunique / shared / weakresource leak avoidedunique ownership默认 ownershared ownership确有共同生命周期custom deleter匹配 release APIresource ledger一次取得一次释放RAII 不只是智能指针:它把资源、锁、注册状态与失败回滚都绑定到对象生命周期
资源获取成功后立即交给 owner,之后由对象析构统一覆盖 return、exception、cancel 与构造失败路径。

资源所有权实验

先预测:哪个退出路径会泄漏?

先判断 acquire 后是否立即进入 owner,再切换 RAII 与 smart pointer 策略查看证据。

观察

raw acquire 后若先执行可能 return/throw 的业务语句,释放责任就藏在控制流里;early return、exception 和新增分支都可能制造 resource leak。

决策

资源成功后立即构造 owner,把 release 绑定到对象生命周期;业务函数只接收借用引用,不把所有权留在注释里。

当前场景 · objects to manage resources

资源成功后立即构造 owner,把 release 绑定到对象生命周期;业务函数只接收借用引用,不把所有权留在注释里。

从“最后一行 delete 为什么不可靠”开始

void processInvestment()
{
    Investment* investment = createInvestment();
    performAudit(*investment); // 可能抛异常
    if (!investment->isValid())
        return;                 // 提前返回
    delete investment;
}

只要函数在 delete 前 return、throw 或新增分支,资源就泄漏。

手工协议把责任藏在控制流里;分支越多,证明越困难。

Item 13 的原则是 Use objects to manage resources(以对象管理资源):把释放责任交给具有确定 destructor 的 owner,而不是交给每条控制流路径记忆。

不只指 heap memory;锁泄漏可变成 deadlock,事务泄漏可占用连接。

RAII 的两条核心原则

原书 Item 13 有两条关键要求:获得资源后立刻放进管理对象;管理对象用 destructor 确保释放。

void processInvestment()
{
    auto investment = createInvestment(); // 返回 unique_ptr
    performAudit(*investment);
    if (!investment->isValid())
        return;
}

正常 return、异常、break 都沿同一对象生命周期清理。

factory 应直接返回 owner

旧接口返回 raw pointer:

Investment* createInvestment();

调用方必须猜测是否拥有、用 delete 还是特定函数释放。现代接口直接表达:

std::unique_ptr<Investment> createInvestment();

若实现失败,局部资源自动回收;成功后 owner 一次 move 给调用方。

业务函数通常接收 Investment&/const Investment&,而不是复制 owner。

unique ownership 是默认起点

using InvestmentPtr = std::unique_ptr<Investment>;
 
InvestmentPtr createInvestment(Kind kind)
{
    if (kind == Kind::Stock)
        return std::make_unique<Stock>();
    return std::make_unique<Bond>();
}

开销通常接近 raw pointer(无状态 deleter 时),并通过类型禁止意外复制。

shared ownership 有额外语义

std::shared_ptr<const Texture> texture = loadSharedTexture(key);

shared_ptr 涉及原子计数、control block、潜在循环和不可预测释放线程。它表达业务共享,不只是防 leak 工具。

用于打破 ownership cycle 或缓存观察。

原书 auto_ptr 的历史位置

原书使用 auto_ptr 说明“对象管理资源”,但其 copy 会让源变 null,无法满足正常值语义,也不能安全放入标准容器。现代对应方案是 unique_ptr 与 move。

auto first = std::make_unique<Investment>();
auto second = std::move(first);
assert(!first);

比 auto_ptr 的隐式“复制即转移”更清晰。

非内存资源也用对象管理

struct FileCloser {
    void operator()(std::FILE* file) const noexcept
    {
        if (file)
            std::fclose(file);
    }
};
 
using FilePtr = std::unique_ptr<std::FILE, FileCloser>;
 
FilePtr openFile(const char* path)
{
    FilePtr file{std::fopen(path, "rb")};
    if (!file)
        throw std::system_error(errno, std::generic_category());
    return file;
}

不要用 delete 释放 malloc/fopen/系统句柄。

锁与作用域状态

void updateSharedState()
{
    std::lock_guard lock{mutex};
    mutate();
    validate(); // 抛异常时仍解锁
}

RAII 的本质是作用域绑定,不要求资源一定是 pointer。

构造失败时已完成成员自动回滚

class Session {
public:
    Session()
        : socket_{openSocket()},
          buffer_{allocateBuffer()},
          registration_{registerSession()} {}
private:
    SocketOwner socket_;
    BufferOwner buffer_;
    RegistrationOwner registration_;
};

若 registration 构造失败,buffer/socket owner 自动析构。Session destructor 不会运行,因为完整 Session 未构造。

比手写 try/catch 清理列表更可靠。

destructor 仍必须不抛

Item 8 的原则直接适用。若 close/commit 失败需要调用方处理,提供显式接口;destructor 只做 no-throw fallback。

class Transaction {
public:
    void commit();
    ~Transaction() noexcept { rollback_if_needed_noexcept(); }
};

不能因 RAII 就把所有动作塞进 destructor。

避免 owner 与 borrower 混淆

void consume(std::unique_ptr<Job> job);       // 取得 unique ownership
void retain(std::shared_ptr<Job> job);        // 共享 ownership
void inspect(const Job& job);                 // borrow
void maybeInspect(const Job* job);            // nullable borrow

不要所有参数都写 shared_ptr 或 raw pointer 再靠注释解释。

*ptr/ptr.get() 只应在 owner 保证存活的范围内使用;不能把 borrow 保存得比 owner 更久。

循环 shared ownership

struct Node {
    std::vector<std::shared_ptr<Node>> children;
    std::weak_ptr<Node> parent;
};

父拥有子,子只观察父,明确方向。若图语义无法定义 owner,考虑 arena/graph owner 与 raw/weak handles,而非任意 shared。

常比复杂 shared 环更容易证明生命周期。

失败注入证明所有路径释放一次

先预测:在 Session 第二、第三个成员构造时抛异常,哪些 owner destructor 执行?完整构造后业务函数抛异常又如何?

  • 每个成功 acquire 最终恰好一个 matching release。
  • 第 N 个成员构造失败时此前资源逆序释放。
  • early return、exception、cancel 都无需手写 cleanup。
  • unique owner move 后源为空,目标唯一释放。
  • shared/weak cycle 测试中最后 owner 释放后资源计数归零。
  • deleter 故障不逃离 destructor,并按 Item 8 策略记录。

小结

  • 手工释放依赖每条控制路径,early return 与 exception 容易泄漏
  • 资源获取后立即交给管理对象,destructor 统一正常和异常退出
  • factory 应直接返回 owner,业务函数通常只接收 borrow
  • 默认选择 value 或 unique ownership,确有共同生命周期才使用 shared ownership
  • custom deleter、lock_guard 和 scope guard 把非内存资源也纳入 RAII
  • 失败注入与 resource ledger 证明每个成功获取都有且只有一次释放

资料与写作方式声明

本章以Effective C++, Third Edition, Item 13权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

名词解释

本章出现的专业名词,用大白话再讲一遍。

resource ownership

取得资源后承担配对释放的责任。

resource leak

资源不再需要后未被归还或结束。

RAII

把资源获取与管理对象初始化绑定的习惯。

immediate ownership transfer

获取成功后立即建立 owner 的规则。

deterministic cleanup

作用域结束或栈展开时自动执行的释放。

owning factory

创建资源并返回已建立 owner 的函数。

borrowed access

不延长或结束生命周期的短期资源访问。

unique ownership

同一时刻只有一个可 move owner 的语义。

unique_ptr

以 deleter 管理独占 pointer 资源的标准类型。

shared ownership

多个 owner 以引用计数共同延长生命周期。

control block

保存 shared/weak 计数与 deleter 的结构。

weak_ptr

不增加 owner 计数的 shared resource 观察句柄。

auto_ptr

以复制语法转移所有权、已删除的旧智能指针。

ownership move

显式把 unique owner 从源转给目标。

custom-deleter owner

将非 new 句柄与配对释放函数绑定的 owner。

paired release protocol

资源必须使用对应 API 家族释放的约定。

lock_guard

构造加锁、析构解锁的 RAII guard。

scope guard

离开作用域时执行注册清理动作的对象。

partial-construction cleanup

构造失败时已完成子对象自动逆序析构。

memberwise rollback

RAII 成员按获取逆序自动撤销构造步骤。

no-throw release

释放动作不让异常越过析构边界的保证。

explicit resource commit

需要调用方显式观察的可失败提交。

ownership-signaling API

签名直接表达取得、共享或借用责任的接口。

borrow projection

从 owner 产生短期裸引用但不转移所有权。

ownership cycle

shared owner 相互持有导致计数不归零的环。

aggregate owner

单一图或 arena 对象集中拥有全部节点的模型。

resource ledger

记录 acquire/release 身份与次数的测试工具。

练习

  1. 问题 1:objects to manage resources 与 raii。 函数依次 new 对象、打开文件、加锁,中途多处 return/throw,设计 RAII 结构。
  1. 问题 2:smart pointer 与 resource leak。 比较纹理缓存、独占 socket、父子节点图和只读函数参数应使用什么类型。
  1. 问题 3:验证构造回滚。 Session 有 socket、buffer、registration 三个 RAII 成员,设计每一步失败测试。

讨论

评论区加载中…