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 失败测试,证明资源只释放一次
Resource copy policy复制语法必须服从所有权、可变性与释放责任copying behavior inresource-managing classespolicy firstprohibit copyingidentity-bounddelete copyreference countingshared identityweak breaks cyclesdeep copyindependent identityor transfer ownershipcopysource remains ownermovesource valid emptyowned candidate失败时目标不变resource ledger身份 / 计数 / release同一资源共享、独立克隆或转移责任,必须在接口和测试中显式表达
先确定资源复制策略,再实现特殊成员;copy、move、计数、克隆和释放次数都要有可观察证据。

资源复制策略实验

先预测:复制后谁还负责释放?

先判断资源身份与所有权方向,再切换策略查看 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;更根本的问题是“复制一次持锁责任”没有自然语义。

必须在实现特殊成员前确定。 Think carefully about copying behavior in resource-managing classes(审慎考虑资源管理类的复制行为)要求 copy 与 move 分开决策:资源没有独立副本语义,不代表它一定不能安全转移;反之,可复制 owner 也必须说明复制是共享还是深拷贝。

策略一:禁止复制

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 的同步和修改可见性必须另行设计。

reference counting 不等于垃圾回收。

weak observation 打破反向拥有

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 的多态资源

class PixelBuffer {
public:
    virtual ~PixelBuffer() = default;
    virtual std::unique_ptr<PixelBuffer> clone() const = 0;
};

每个派生实现 clone。测试不能只比较基类可见字段,还要验证动态类型和派生资源独立。

是 deep copy 的关键 oracle。

策略四:转移所有权的历史与现代形式

原书讨论 auto_ptr:复制语法实际转移 ownership。现代已删除,改用 explicit move。

违反普通 copy 预期,难以用于容器和泛型代码。

auto source = std::make_unique<Resource>();
auto target = std::move(source);
assert(!source);

调用点清楚标示源可能改变。

copy assignment 的 prepare-then-commit

错误实现先释放旧资源再 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 时必须先增加新资源计数,再减少旧资源,否则同一资源可能计数归零被销毁。

void assignShared(Control* next)
{
    addRef(next);
    release(current_);
    current_ = next;
}

优先标准 shared_ptr,除非 ABI、分配或外部协议要求 intrusive。

copy-on-write 的隐含复杂度

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 的可执行文档。

复制策略测试矩阵

先预测:禁止、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

资料与写作方式声明

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

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

名词解释

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

resource-managing class

直接拥有并释放资源的类。

resource copy policy

复制后资源禁止、共享、克隆或转移的语义。

prohibit copying

删除 copy constructor/assignment 的策略。

identity-bound resource

责任与实例身份绑定、不能复制的资源。

ownership transfer

把唯一资源责任从源交给目标的行为。

moved-from owner state

转移后仍可析构赋值的源 owner 状态。

reference counting

复制增加共享计数、最后 owner 释放的策略。

shared-resource identity

复制后源目标观察同一资源身份。

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. 问题 1:copying behavior in resource-managing classes:prohibit copying 与 reference counting。 为 mutex lock、immutable texture、editable image、file handle 分别选择复制行为。
  1. 问题 2:deep copy 与 transfer ownership。 多态 PixelBuffer::clone 可抛异常,设计 self-safe 强保证。
  1. 问题 3:修复共享环。 Parent/Child 互持 shared_ptr 导致泄漏,设计 owner 方向与测试。

讨论

评论区加载中…