Item 28:避免返回指向对象内部的句柄

对齐 Effective C++ 第三版 Item 28:识别 reference、pointer、iterator 和 view 返回如何泄漏内部表示、绕过 const 与制造悬空句柄,并以值快照、受控命令、稳定 ID 和作用域借用替代。

学习目标

  • 能解释 const member 返回 mutable reference 如何绕过 owner 的 encapsulation 与 invariant
  • 能复现 reference、pointer、iterator 在临时对象销毁、容器重分配和并发修改后的 dangling handle
  • 能设计 value snapshot、owner command、stable ID、scoped callback 与 ref-qualified accessor 的安全契约
Internal handle boundary mapreference · pointer · iterator → lifetime / representation / synchandles to object internalsreference pointer iterator地址 / layout 泄漏dangling handleowner / storage changedreallocation / erase / destroyencapsulationsnapshot / commandcallback / stable ID每个出口都要回答owner 谁持有何时失效谁能写锁是否覆盖读取安全性优先于省下一次复制;长期 identity 使用稳定逻辑句柄
内部句柄把地址、表示、生命周期和同步泄漏给调用者;快照、命令、作用域借用和稳定 ID 提供不同边界。

句柄边界实验

谁拥有这段地址

先预测 owner mutation 后 handle 是否仍有效,再切换场景查看替代契约和验证证据。

暴露风险

public API 把 reference pointer iterator 交给调用者,内部容器、地址和写权限变成外部依赖。

替代契约

只读查询返回 value snapshot;修改走 owner-mediated command;短借用使用 scoped callback。

当前场景 · reference / pointer / iterator

只读查询返回 value snapshot;修改走 owner-mediated command;短借用使用 scoped callback。

从 const Rectangle 被外部修改开始

Rectangle 把两个角点放在内部数据对象:

class Point {
public:
    void setX(int value);
    void setY(int value);
};
 
struct RectData {
    Point upperLeft;
    Point lowerRight;
};
 
class Rectangle {
public:
    Point& upperLeft() const { return data_->upperLeft; }
 
private:
    std::shared_ptr<RectData> data_;
};

成员函数标记 const,但返回 Point&。调用者可通过 const Rectangle 修改内部 Point:

const Rectangle box = makeRectangle();
box.upperLeft().setX(1000);

Item 28 的原则是 Avoid returning handles to object internals(避免返回指向对象内部的句柄)。

const member 只约束 this 访问路径

upperLeft() const 表示函数不能通过 this 修改 Rectangle 的非-mutable members;它不自动改变返回类型。

返回 Point& 是重新授予写权限。调用者可绕过 Rectangle 对角点顺序、边界和同步的规则。

这同时破坏 logical constness:用户观察到 const Rectangle 仍能改变几何值。

handle 不只指针和引用

原书将 reference、pointer、iterator 都视为 handles。现代代码还包括 span、string_view、range view、proxy reference 和 raw native handle。

class Catalog {
public:
    std::vector<Item>::iterator begin();
    Item* findMutable(ItemId id);
    std::span<Item> items();
};

这些 API 让调用者直接修改 Items、保留地址并依赖 vector representation。private vector 已事实上成为 public contract。

即使返回 const_iterator/span<const Item>,写权限下降,但 lifetime、invalidation 与表示耦合仍存在。

改成 const reference 只解决写权限

const Point& Rectangle::upperLeft() const & {
    return data_->upperLeft;
}

调用者不能通过该引用调用 Point mutator,encapsulation 更强;但引用仍依赖 Rectangle/RectData 的生命周期。

若 owner 销毁、替换 data_ 或 copy-on-write detach,引用可能失效。

因此 const reference 不是普适终点,只适合 lifetime 明确、性能证据充分的局部借用。

临时 owner 会立刻制造悬空引用

const Point* corner = &makeRectangle().upperLeft();
// full-expression 结束,temporary Rectangle 销毁
use(corner->x()); // dangling

返回 borrow 不延长完整对象生命周期。shared pimpl 也不能自动救场:若 handle 只是 Point reference,没有携带 shared owner,RectData 仍可随临时 Rectangle 释放。

用 ref qualifier 限制临时对象调用

可以只允许 lvalue owner 返回 borrow,并让 rvalue 返回 value:

class Rectangle {
public:
    const Point& upperLeft() const & noexcept;
    Point upperLeft() &&;
};

const & 版本借用稳定 lvalue owner;&& 版本复制/move snapshot,临时销毁后结果仍独立。也可 delete rvalue overload 强制调用者先保存 owner。

ref qualifier 仍不能阻止调用者保存 lvalue borrow 超过 owner lifetime,文档和 static analysis 仍需要。

容器重分配会让 iterator/reference 失效

const Item& selected = catalog.itemAt(0);
catalog.add(Item{/*...*/}); // vector 可能 reallocate
use(selected);              // 可能 dangling

reserve 能降低概率但不是永久 contract;insert/erase/sort 也有各自失效或 identity 变化规则。

如果调用者需要长期 identity,返回 ItemId 并在每次操作时解析,或使用 stable node owner,而不是泄漏 vector address。

mutable handle 破坏同步边界

线程安全 owner 在 accessor 内加锁再返回 reference,锁离开函数时释放;调用者随后使用引用时没有保护。

const Item& Catalog::first() const {
    std::scoped_lock lock(mutex_);
    return items_.front();
} // lock released, reference escapes

返回 value snapshot,或提供在锁内执行 callback 的 withFirst,才能让保护覆盖实际读取。

值返回通常是最清楚的查询契约

Point 很小,直接返回 value:

Point Rectangle::upperLeft() const {
    return data_->upperLeft;
}

copy elision/move 与小型 value type 使成本通常可接受;更重要的是 lifetime、thread safety 和 representation freedom 清楚。

性能热点应测量,不要先用 reference 交换掉安全性。

修改应表达为 owner command

调用者若要移动角点,应请求 Rectangle 执行动作:

Result<void, GeometryError> Rectangle::moveUpperLeft(Point target) {
    if (target.x() > data_->lowerRight.x()) {
        return unexpected(GeometryError::InvertedBounds);
    }
    data_->upperLeft = target;
    return {};
}

owner 可以加锁、维护 cache、记录事件并拒绝非法状态。

大对象借用可限制在 callback scope

若复制很贵且调用只需短时读取,可让 owner 控制 borrow lifetime:

template<class Function>
decltype(auto) Catalog::withItems(Function&& function) const {
    std::scoped_lock lock(mutex_);
    return std::forward<Function>(function)(
        std::span<const Item>{items_});
}

callback 不应把 span/reference 保存到外部;可通过 API convention、borrow checker-like static tools 或返回类型限制降低风险。

stable ID 代替长期内部地址

编辑器、entity system 或数据库界面常需要跨帧引用。返回 address 不稳定,返回 generation-tagged ID 更适合。

struct ItemId {
    std::uint32_t index;
    std::uint32_t generation;
};
 
Result<ItemSnapshot, LookupError> Catalog::get(ItemId id) const;

owner 可重排 storage,同时保持 ID contract;删除后旧 generation 明确失败,而不是悬空解引用。

shared ownership 只在真实共享生命周期时使用

若调用者确实需要独立延长某对象生命周期,可返回 shared_ptr<const Item>,但这改变 ownership 和分配模型。

不要仅为避免 copy 把所有内部对象改成 shared ownership;它增加 reference count、cycle 和 identity complexity。

如果只需 snapshot,value 更简单;如果只需短借用,callback/view 更准确。

用失效矩阵验收所有出口

先预测每个返回值在 owner 销毁、move、mutation、reallocation 和并发时是否仍有效,再建立门禁:

  • 搜索所有 public reference/pointer/iterator/span/string_view 返回。
  • compile-negative tests 证明 const owner 不发布 mutable internal handle。
  • temporary owner tests 验证 rvalue accessor 返回 value 或被删除。
  • container tests 覆盖 reserve、reallocation、insert、erase、sort 后 handle 状态。
  • concurrency tests 验证没有 escaped-lock handle,snapshot/callback 受同步保护。
  • ID tests 覆盖 delete/reuse/generation mismatch。
  • lifetime sanitizer 捕获 owner destruction 后的 reference/view 使用。
  • representation swap 将 vector 换为其他 storage,value/ID clients 不修改。

任何长期 borrow 都必须有比“调用者小心”更强的证据。

小结

  • reference、pointer、iterator、span/view 都是 handles,可能把 private representation 变成 public dependency
  • const member 不会自动让返回 handle 只读;mutable reference 可绕过 owner invariant
  • 改成 const reference 只修复写权限,owner 销毁或 storage 变化仍会产生 dangling handle
  • rvalue accessor 应返回 detached value 或禁止调用,避免临时 owner 发布 borrow
  • value snapshot、owner command、scoped callback、stable ID 分别解决观察、修改、短借用与长期 identity
  • 用 handle invalidation matrix 覆盖生命周期、重分配、同步和 generation reuse

资料与写作方式声明

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

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

名词解释

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

object handle

可间接访问另一个对象的 reference/pointer/iterator。

internal handle exposure

public API 发布 private subobject 别名。

const this access path

const member 对 this 的只读限制。

invariant bypass alias

绕过 owner operation 修改内部状态的别名。

logical constness boundary

用户可观察语义保持不变的 const 边界。

iterator handle

保存容器内部位置的遍历句柄。

representation leakage

handle 让调用者依赖内部容器与布局。

read-only internal borrow

只读但依赖 owner storage 的返回别名。

handle invalidation event

使既有 handle 不再有效的 owner 事件。

dangling handle

所借对象生命周期结束后的残留句柄。

temporary owner destruction

完整表达式结束时临时 owner 销毁。

reference-qualified accessor

按调用对象 value category 限制 accessor。

detached value snapshot

与 owner lifetime 无关的独立结果值。

reallocation invalidation

backing storage 更换导致地址失效。

invalidation contract

规定哪些 owner 操作使 handle 失效。

escaped-lock handle

保护锁释放后仍逃逸使用的内部别名。

synchronized access scope

实际访问与锁处于同一作用域。

value query
返回调用者拥有的独立查询结果。
representation-independent result

不依赖内部地址布局的结果。

owner-mediated command

由 owner 验证并提交内部修改的操作。

mutation-path closure

所有内部写路径均由 owner 掌控。

scoped borrow callback

在 owner 控制调用期间提供短借用。

non-escaping borrow contract

借用不得离开受控访问作用域。

stable logical handle

每次由 owner 验证解析的非地址 ID。

generation counter

识别 slot 删除复用的 ID 版本。

lifetime-extending owner return

通过 owner 延长 pointee 生命周期的返回。

genuine shared lifetime

多个组件共同决定销毁时机的语义。

handle invalidation matrix

handle 权限、失效与同步验证表。

练习

  1. 问题 1:修复 Rectangle accessor(handles to object internals、encapsulation、reference pointer iterator)。 const member 返回 Point mutable reference,请分别设计观察和修改接口。
  1. 问题 2:解决 Catalog 并发引用(dangling handle、encapsulation、reference pointer iterator)。 accessor 持锁返回 const Item&,另一线程 add 触发 reallocation,请重构。
  1. 问题 3:审查长期编辑句柄(dangling handle、stable logical handle、generation counter)。 UI 跨帧保存 vector iterator 指向选中实体,删除和排序后偶发编辑错对象,请设计稳定方案。

讨论

评论区加载中…