Item 14:审慎考虑资源管理类的复制行为
对齐 Effective C++ 第三版 Item 14:为 resource-managing classes 选择禁止复制、引用计数、深拷贝或转移所有权,并实现异常安全赋值与 shared cycle 防护。
学习目标
- 能比较禁止复制、reference counting、deep copy 与 ownership transfer 四种资源管理类复制策略
- 能实现异常安全的资源 copy constructor/assignment,处理 self-alias、旧资源释放和独立/共享身份
- 能设计 shared ownership cycle、move 后源状态与 clone 失败测试,证明资源只释放一次
资源复制策略实验
先预测:复制后谁还负责释放?
先判断资源身份与所有权方向,再切换策略查看 copy、move 和异常路径证据。
观察
resource-managing class 的复制不是默认语法问题,而是资源身份、可变性和释放责任的业务策略;错误复制 Lock 可能让两个 destructor 操作同一责任。
决策
先为资源写复制策略,再决定特殊成员:prohibit copying、reference counting、deep copy 或 transfer ownership;copy 与 move 分开审查。
当前场景 · copying behavior in resource-managing classes
先为资源写复制策略,再决定特殊成员:prohibit copying、reference counting、deep copy 或 transfer ownership;copy 与 move 分开审查。
从“复制一个锁意味着什么”开始
Item 13 建立 RAII owner 后,编译器可能继续为管理类生成 copy。假设:
class Lock {
public:
explicit Lock(Mutex* mutex) : mutex_{mutex}
{
lock(mutex_);
}
~Lock() { unlock(mutex_); }
private:
Mutex* mutex_;
};默认 copy 只复制 mutex pointer,两个 Lock destructor 都 unlock 同一 mutex;更根本的问题是“复制一次持锁责任”没有自然语义。
↡直接拥有并释放资源的类,其 copy 行为必须与资源的业务所有权语义一致。 ↡规定复制后资源是禁止、共享、独立克隆还是转移给目标的业务含义。必须在实现特殊成员前确定。 Think carefully about copying behavior in resource-managing classes(审慎考虑资源管理类的复制行为)要求 copy 与 move 分开决策:资源没有独立副本语义,不代表它一定不能安全转移;反之,可复制 owner 也必须说明复制是共享还是深拷贝。
策略一:禁止复制
↡类型明确删除 copy constructor 与 copy assignment,使复制表达式编译失败。class Lock {
public:
explicit Lock(Mutex& mutex) : mutex_{mutex} { mutex_.lock(); }
~Lock() noexcept { mutex_.unlock(); }
Lock(const Lock&) = delete;
Lock& operator=(const Lock&) = delete;
private:
Mutex& mutex_;
};锁、线程、事务、唯一注册 token 通常属于身份责任,复制无法产生两个独立等价 owner。
↡资源责任与某个对象实例、地址或执行上下文绑定,不能表示成独立值的语义。Item 6 的 deleted function 是直接工具。
禁止 copy 不等于禁止 move
↡把唯一资源责任从源对象显式交给目标,源不再负责释放的行为。文件句柄等 unique owner 可 move:
class FileOwner {
public:
FileOwner(const FileOwner&) = delete;
FileOwner& operator=(const FileOwner&) = delete;
FileOwner(FileOwner&& other) noexcept
: handle_{std::exchange(other.handle_, invalidHandle)} {}
FileOwner& operator=(FileOwner&& other) noexcept
{
if (this != &other) {
close_noexcept();
handle_ = std::exchange(other.handle_, invalidHandle);
}
return *this;
}
private:
NativeHandle handle_{invalidHandle};
};锁 guard 是否可 move 取决于 mutex API 与作用域责任,不能套用文件 owner。
策略二:reference counting
↡复制管理对象时增加共享计数,所有副本共同拥有同一资源,最后一个析构释放。class SharedImage {
public:
explicit SharedImage(std::shared_ptr<const PixelBuffer> pixels)
: pixels_{std::move(pixels)} {}
private:
std::shared_ptr<const PixelBuffer> pixels_;
};适合不可变纹理、共享配置等真正共同生命周期。若资源可变,多个 owner 的同步和修改可见性必须另行设计。
↡当多个 shared owner 最终形成闭环,引用计数无法归零导致的资源泄漏。reference counting 不等于垃圾回收。
weak observation 打破反向拥有
↡不增加 strong reference count、通过 lock 临时获得 shared owner 的观察关系。struct Child {
std::weak_ptr<Parent> parent;
};
if (auto parent = child.parent.lock())
parent->notify();如果每条边都“可能 owner”,图会难以证明;优先定义明确主 owner 和观察方向。
策略三:deep copy
↡复制时为目标创建独立资源表示,源目标之后可独立修改和销毁。class Image {
public:
Image(const Image& rhs)
: pixels_{rhs.pixels_->clone()} {}
Image& operator=(const Image& rhs)
{
auto candidate = rhs.pixels_->clone();
pixels_.swap(candidate);
return *this;
}
private:
std::unique_ptr<PixelBuffer> pixels_;
};clone 可能分配并抛异常,assignment 先准备 candidate 再 no-throw commit。
deep copy 的多态资源
↡复制实际派生资源并保留 dynamic type 的 virtual clone 接口。class PixelBuffer {
public:
virtual ~PixelBuffer() = default;
virtual std::unique_ptr<PixelBuffer> clone() const = 0;
};每个派生实现 clone。测试不能只比较基类可见字段,还要验证动态类型和派生资源独立。
↡复制后目标资源与源内容等价,但具有不同资源身份。是 deep copy 的关键 oracle。
策略四:转移所有权的历史与现代形式
原书讨论 auto_ptr:复制语法实际转移 ownership。现代已删除,改用 explicit move。
↡旧 auto_ptr 以 copy constructor/assignment 语法清空源并转移资源的非常规语义。违反普通 copy 预期,难以用于容器和泛型代码。
auto source = std::make_unique<Resource>();
auto target = std::move(source);
assert(!source);调用点清楚标示源可能改变。
copy assignment 的 prepare-then-commit
↡在目标不变时先取得新资源 owner,成功后才替换目标旧资源的赋值结构。错误实现先释放旧资源再 clone,失败后目标损坏。正确顺序:
Image& Image::operator=(const Image& rhs)
{
auto candidate = rhs.pixels_->clone();
pixels_.swap(candidate);
return *this;
}self-assignment 时 clone 从仍有效源读取;异常时 this 不变;提交后旧 owner 在 candidate 析构释放。
共享赋值也要注意顺序
shared_ptr assignment 自身正确处理引用计数与 self-assignment。手写 intrusive count 时必须先增加新资源计数,再减少旧资源,否则同一资源可能计数归零被销毁。
↡先保住新 shared resource 的计数,再释放旧 owner,防止 alias 时资源提前销毁。void assignShared(Control* next)
{
addRef(next);
release(current_);
current_ = next;
}优先标准 shared_ptr,除非 ABI、分配或外部协议要求 intrusive。
copy-on-write 的隐含复杂度
↡副本先共享资源,首次写入时若有多个 owner 再克隆独立资源的策略。void Image::ensureUnique()
{
if (!pixels_.unique())
pixels_ = std::make_shared<PixelBuffer>(*pixels_);
}COW 结合 shared/deep copy,增加线程安全、iterator/reference 失效和写路径异常。只有性能证据支持时使用。
rule of zero 组合资源策略
↡选择具有正确复制语义的成员类型,让外层管理类使用编译器生成特殊成员。unique_ptr 自动禁止 copy、允许 move;shared_ptr 自动 reference count;自定义 CloneValue 可封装 deep copy。外层不再手写重复资源逻辑。
class TextureHandle {
private:
std::shared_ptr<const Texture> texture_;
};成员类型就是资源 copy policy 的可执行文档。
复制策略测试矩阵
↡记录源目标资源地址、owner count、内容和 release 次数,用于验证复制语义的测试。先预测:禁止、shared、deep、move 四种策略复制后 source/target 的资源地址、内容、计数和源状态分别如何。
- prohibit:copy expression compile-fail,move 按协议单独验证。
- reference count:地址相同、strong count 增加,最后 owner 释放一次。
- deep copy:内容相等、地址不同,修改目标不改变源。
- move:目标取得原地址,源进入有效空状态。
- clone 第 N 步失败,assignment 目标保持原资源。
- shared cycle 使用 weak/aggregate owner 后外部释放可使 ledger 归零。
小结
- resource-managing class 的 copy 必须先选择业务资源语义
- 无合理副本时删除 copy;独占 owner 可按协议支持 move
- reference counting 表达共享生命周期,但需防 ownership cycle
- deep copy 通过 clone 建立独立资源,assignment 用 prepare-then-commit
- auto_ptr 的 copy-as-transfer 已被 explicit unique_ptr move 取代
- 优先用成员类型编码策略,让外层回到 rule of zero
名词解释
本章出现的专业名词,用大白话再讲一遍。
- resource-managing class
直接拥有并释放资源的类。
- resource copy policy
复制后资源禁止、共享、克隆或转移的语义。
- prohibit copying
删除 copy constructor/assignment 的策略。
- identity-bound resource
责任与实例身份绑定、不能复制的资源。
- ownership transfer
把唯一资源责任从源交给目标的行为。
- moved-from owner state
转移后仍可析构赋值的源 owner 状态。
- reference counting
复制增加共享计数、最后 owner 释放的策略。
- ownership cycle
shared owners 形成计数不归零的闭环。
- weak observation
不增加 strong count 的 shared resource 观察。
- expired observation
weak lock 失败表示资源已经销毁。
- deep copy
为目标创建独立资源表示的复制策略。
- clone operation
创建同动态类型独立资源副本的操作。
- polymorphic deep copy
通过 virtual clone 保留动态类型的复制。
- independent resource identity
复制后内容等价但资源地址不同。
- copy-as-transfer
旧 auto_ptr 用复制语法转移所有权的语义。
- explicit move ownership
以 move 明确转移 unique owner 的语义。
- resource assignment transaction
候选资源成功后才替换目标的赋值。
- owned candidate
尚未提交、由局部 owner 管理的新资源候选。
- increment-before-decrement
共享赋值先增新计数再减旧计数的顺序。
- intrusive reference counting
计数存于资源对象内的共享模型。
- copy-on-write
副本共享至首次写入再克隆的策略。
- detach-before-write
写前确认唯一 owner 或复制资源的步骤。
- policy-by-member-type
用成员类型直接编码资源复制策略。
- rule of zero
由成员组合且不声明特殊成员的设计。
- resource-copy ledger
记录复制资源身份、计数与释放的测试。
练习
- 问题 1:copying behavior in resource-managing classes:prohibit copying 与 reference counting。 为 mutex lock、immutable texture、editable image、file handle 分别选择复制行为。
- 问题 2:deep copy 与 transfer ownership。 多态 PixelBuffer::clone 可抛异常,设计 self-safe 强保证。
- 问题 3:修复共享环。 Parent/Child 互持 shared_ptr 导致泄漏,设计 owner 方向与测试。