Item 15:在资源管理类中提供 raw resource access

对齐 Effective C++ 第三版 Item 15:通过 get/native_handle、operator* 与 operator-> 提供短期 raw resource borrow,比较显式和隐式转换,并维护 ownership 与生命周期。

学习目标

  • 能解释 resource-managing class 暴露 raw resource 为何不等于转移 ownership,并设计 borrow lifetime 契约
  • 能比较 named getter、operator*、operator->、explicit/implicit conversion 与 release 的调用便利和误用风险
  • 能实现 const 正确的 native_handle/get 接口,验证 owner move/reset、异步逃逸和 deleter 配对

从“管理资源不等于隐藏资源”开始

许多既有 API 只接受 raw handle:

void configureFont(FontHandle* font, int size);
 
FontOwner font = loadFont("Inter.ttf");
configureFont(font.get(), 16);

资源管理类负责取得、转移和释放;业务操作仍可能需要原始 resource。

Item 15 要求 Provide access to raw resources in resource-managing classes(在资源管理类中提供 raw resource access)。

接口必须让两者区别清晰。

raw access 是 borrow projection:外部 API 不取得 ownership,owner move/reset/destroy 后借用失效。

named getter 最明确

class FontOwner {
public:
    FontHandle* get() noexcept { return handle_; }
    const FontHandle* get() const noexcept { return handle_; }
private:
    FontHandle* handle_{};
};

调用点明确跨入 raw API,便于搜索和审计。

是否传播 const 取决于 owner 语义。标准 unique_ptr 的 const 对象仍可修改 pointee,因为 const 约束 owner 自身;自定义 wrapper 可选择更强 deep const,但需一致。

operator* 与 operator-> 提供 pointer 体验

class InvestmentPtr {
public:
    Investment& operator*() const noexcept { return *ptr_; }
    Investment* operator->() const noexcept { return ptr_; }
};

适合智能指针,但 raw integer handle 没有自然解引用语义,优先 native_handle()

named getter 最清楚;operator 星号箭头保留 pointer 体验;implicit conversion 便利但扩大误用面。

implicit conversion 的便利与风险

class FontOwner {
public:
    operator FontHandle*() const noexcept { return handle_; }
};

可直接传给 legacy API,却扩大重载选择和误用:

FontOwner font = loadFont("Inter.ttf");
delete font; // 若转换为 pointer 后编译通过,将绕过正确 deleter

原书承认 implicit conversion 更方便,但 named access 更安全。现代默认选择 get()/native_handle()

explicit conversion 是折中

explicit operator FontHandle*() const noexcept { return handle_; }

调用仍比 get 隐晦:static_cast<FontHandle*>(font)。named getter 能表达“我现在借用 native resource”,通常更可读。

void configure(FontOwner& font, int size)
{
    configureFont(font.get(), size);
}

减少 raw handle 传播范围。

get 与 release 完全不同

auto owner = std::make_unique<Widget>();
Widget* borrowed = owner.get();
Widget* transferred = owner.release();

borrowed 不可 delete;transferred 必须由新 owner 立即接管,否则泄漏。

std::unique_ptr<Widget, CustomDelete> next{previous.release()};

还需确保 deleter 协议兼容,不能只转 pointer 忘记释放方式。

raw borrow 的生命周期

Widget* raw = owner.get();
owner.reset();
raw->work(); // use-after-free

owner destroy/reset 直接结束资源;owner move 后资源通常仍在新 owner 中,但从旧 owner 取得的 raw pointer 是否继续可用取决于资源本身是否移动地址。

raw handle 不携带生命周期;异步或长期保存必须改为 shared/weak/token 等明确协议。

Lab

Item 15 raw resource 生命周期实验

先预测 owner、borrow 和释放责任,再切换 get、explicit conversion 与异步逃逸样本。

get function 导出短期 raw resource,owner 仍负责释放

owner=alive → get=borrow → call=sync → owner=releases-once → raw=not-owning

判定

accept:access to raw resources 边界清楚

当前样本:get borrow;保存 owner 身份、borrow 时间窗、conversion 形式、deleter 和复位轨迹。

异步 API 不能只借用

register_callback(owner.get()); // 若 callback 长期保存 pointer,局部 owner 结束后悬空

需要 shared ownership、weak observation、registration token 或外层 aggregate owner。

同步调用期间 borrow 安全,不代表异步保存安全。

raw access 不应暴露 deleter 控制

返回 pointer 不应同时允许调用方改变 deleter、control block 或 ownership count。管理对象的封装目标主要是资源管理责任,而不是禁止所有资源操作。

这解释了为何 raw accessor 与封装并不矛盾。

integer/native handle 的无效值

class SocketOwner {
public:
    NativeSocket native_handle() const noexcept { return socket_; }
    explicit operator bool() const noexcept { return socket_ != invalidSocket; }
private:
    NativeSocket socket_{invalidSocket};
};

accessor 可返回 sentinel,调用方先检查 bool;或在无效时抛异常/返回 optional,按 API 风格决定。

accessor 的异常与 noexcept

get/native_handle/operator-> 通常 noexcept。若访问需要延迟加载或锁,命名应反映可能失败,不能伪装成简单 get。

Resource& get();                 // 简单已存在访问
expected<Resource&, Error> load(); // 可失败获取

Item 15 讨论访问已管理资源,不是让 get 偷偷取得新资源。

防止错误 delete 的类型设计

class FontView {
public:
    explicit FontView(FontHandle* handle) : handle_{handle} {}
    FontHandle* native_handle() const noexcept { return handle_; }
private:
    FontHandle* handle_{};
};

typed view 仍不自动跟踪生命周期,但可限制 API 集合、附带 generation 检查或线程约束。

适合调试复杂池/arena,不替代正确所有权。

raw access 审计与测试

先预测:get()release() 与 implicit conversion 分别会让 owner 在调用后处于什么状态,谁最终释放资源。

  • get 后 owner 仍有效,borrow 与 owner 指向同一资源。
  • 外部 API 不保存、不 delete borrow;resource ledger 仅 owner 释放一次。
  • const owner 导出权限符合 deep/shallow const 设计。
  • reset/destroy 后 debug generation 检测 stale borrow。
  • 异步注册使用 token/weak/shared,局部 raw 不逃逸。
  • release 后原 owner 为空,raw 立即被兼容 deleter 的新 owner 接管。

小结

  • resource-managing class 可提供 raw access,因为外部 API 仍需操作底层资源
  • get/native_handle 明确表达 borrow,operator*/-> 适合 pointer-like wrapper
  • implicit conversion 方便但扩大 accidental conversion,默认优先 named getter
  • get 保留 ownership,release 转移 ownership,语义不可混淆
  • raw borrow 只在 owner 存活且资源未替换期间有效,异步保存需升级生命周期协议
  • raw-access audit 与 resource ledger 证明调用方不释放、不越期使用

资料与写作方式声明

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

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

名词解释

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

raw resource
由 RAII owner 管理并传给底层 API 的原始句柄。
raw resource borrow
临时访问 raw resource 但不取得释放责任。
explicit raw accessor
以 get/native_handle 明确返回 raw resource 的函数。
const-propagating access
owner const 属性传播到底层访问权限的规则。
dereference access
通过 operator* 返回底层资源引用的访问。
pointer-like access
通过 operator-> 访问底层对象成员的语法。
implicit raw conversion
无需显式调用自动转换为 raw resource。
accidental conversion path
自动转换进入非预期重载或 delete 的路径。
explicit raw conversion
需要显式 cast/context 的 raw 转换。
raw boundary adapter
集中 legacy raw 调用并向业务暴露安全接口的层。
get semantics
导出 borrow 且 owner 保留释放责任的语义。
release semantics
导出 raw 并把释放责任交给调用方的语义。
immediate re-ownership
release 后立即建立新 owner 的规则。
borrow lifetime
raw borrow 可安全访问的时间窗口。
stale raw access
资源失效后继续使用旧 borrow 的错误。
escaping asynchronous borrow
函数返回后仍被异步代码保存使用的借用。
registration lifetime token
析构时注销 callback 的 RAII 注册对象。
deleter state
owner 保存的资源释放函数及其配置。
ownership encapsulation
开放资源操作但保留创建转移释放控制。
native handle
系统 API 的原始句柄和无效值约定。
invalid handle sentinel
表示当前没有有效资源的 null/descriptor 值。
noexcept raw access
只读 handle 且保证不抛的访问函数。
accessor side effect
get 隐藏 I/O、创建或状态修改的设计。
typed borrow handle
限制能力但不拥有资源的专用 view 类型。
borrow generation check
debug borrow 校验 owner 资源版本未改变的机制。
raw-access audit
标注所有 raw 调用保存、释放和失效条件的审计。

练习

  1. 问题 1(access to raw resources、get function):选择访问形式。 为 unique_ptr-like 对象、整数 socket 和 legacy implicit API 分别设计 raw access。
  1. 问题 2(explicit conversion、implicit conversion):修复异步逃逸。 callback API 保存 owner.get() 到后台线程,设计生命周期协议。
  1. 问题 3(get function、access to raw resources):区分 get/release。 设计 resource ledger 测试证明二者 ownership 结果不同。

讨论

评论区加载中…