Item 11:在 operator= 中处理 self-assignment

对齐 Effective C++ 第三版 Item 11:识别直接和别名导致的 self-assignment,比较 identity test、copy-before-delete 与 copy-and-swap,并验证异常安全和 self-move。

学习目标

  • 能复现 delete-before-copy 在直接或间接 self-assignment 中造成的 use-after-free、dangling pointer 与 double delete
  • 能比较 identity test、copy-before-delete、copy-and-swap 与 rule of zero 的正确性、异常保证和成本
  • 能设计覆盖 pointer/reference/base alias、异常注入与 self-move 的 assignment 测试,证明目标保持合法
Self-assignment / candidate commit先读取仍有效的源,再用不会破坏不变量的提交替换旧状态assignment to selfoperator=direct / alias sourceidentity testthis == &rhslocal guard onlycopy and swapcopy-swapprepare candidatecommitno-throw swapold state retiresaliasingpointer / view / basestrong guarantee失败时目标不变self-move有效且可析构resource countcreate / destroy 平衡identity test 只挡直接同址;copy and swap / copy-swap 才把准备与提交分开
自赋值的关键不是变量名,而是源是否与目标别名;先准备独立候选,再以可证明的提交替换旧状态。

赋值别名实验

先预测:这个 operator= 会不会先摧毁源?

先判断左右是否别名,再切换策略查看 identity test 与候选提交的证据。

观察

直接写成 x = x,或通过两个 pointer/reference alias 让左右最终指向同一个对象;delete-before-copy 会先摧毁 rhs 仍要读取的资源。

决策

把 operator= 的读取与写入分开,先确认 alias 风险,再选择 identity test 或独立 candidate;不要让不同调用点名称掩盖同一地址。

当前场景 · assignment to self / operator=

把 operator= 的读取与写入分开,先确认 alias 风险,再选择 identity test 或独立 candidate;不要让不同调用点名称掩盖同一地址。

从“不同名字可能是同一个对象”开始

最明显是 w = w,但真实代码更常由 alias 形成:

Widget* px = &widget;
Widget* py = &widget;
*px = *py;

operator= 只看到 thisrhs,不能假定调用点名称不同就没有 alias。

经典错误:先 delete 再 copy

class Bitmap {
public:
    Bitmap& operator=(const Bitmap& rhs)
    {
        delete image_;
        image_ = new Image(*rhs.image_);
        return *this;
    }
private:
    Image* image_{};
};

如果 this == &rhs,delete image* 同时让 rhs.image* 指向已释放对象,下一行从 dangling pointer 复制。

即使不是 self-assignment,new Image 抛异常后 image_ 仍悬空,析构会再次 delete。

策略一:identity test

Bitmap& Bitmap::operator=(const Bitmap& rhs)
{
    if (this == &rhs)
        return *this;
 
    delete image_;
    image_ = new Image(*rhs.image_);
    return *this;
}

修复直接 self-assignment,却没有修复 new 抛异常后目标悬空。

上述实现甚至不满足 basic guarantee。identity test 只处理一个别名分支,不是完整资源提交策略。

identity test 的成本

地址比较通常很小,但如果实现本来能自然安全处理 self-assignment,就无需专门分支。是否保留应由语义和测量决定。

if (this != &rhs) {
    // expensive replacement
}
return *this;

通常比特殊分支更可靠。

策略二:先复制候选,再释放旧资源

Bitmap& Bitmap::operator=(const Bitmap& rhs)
{
    Image* candidate = new Image(*rhs.image_);
    delete image_;
    image_ = candidate;
    return *this;
}

self-assignment 时 candidate 在旧 image 仍有效时创建;分配失败时 this 完全没变。

该单资源实现满足强保证。若提交多成员可能抛,还需 no-throw commit 或更完整候选对象。

策略三:copy-and-swap

class Bitmap {
public:
    Bitmap& operator=(Bitmap rhs)
    {
        swap(rhs);
        return *this;
    }
 
    void swap(Bitmap& other) noexcept
    {
        using std::swap;
        swap(image_, other.image_);
    }
};

按值参数在函数体前 copy/move 构造候选;若失败 this 未变;swap 后旧资源由 rhs 析构。bitmap = bitmap 只是多复制一次,语义安全。

是 self-assignment 与异常安全共同解法。

rule of zero 消除手写风险

class Bitmap {
public:
    explicit Bitmap(Image image) : image_{std::move(image)} {}
private:
    Image image_;
};

如果 Image 是正确值类型,生成 assignment 组合其保证。优先重构 owning raw pointer,而不是不断加防护。

是生成复制自然安全的前提。

alias 不只发生在整个对象

Text& Text::operator=(std::string_view source)
{
    storage_.assign(source); // source 可能指向 storage_ 自身
    return *this;
}

即使 this 与 rhs 类型不同,source 也可能来自 text.view()。修改 storage_ 可先使 source 悬空。

Text& Text::operator=(std::string_view source)
{
    std::string candidate{source};
    storage_.swap(candidate);
    return *this;
}

更一般地,任何 source view 可能重叠目标表示时都先准备独立候选。

基类引用形成间接 self-assignment

Derived object;
Base& left = object;
Base& right = object;
left = right;

Base::operator= 应对自身 base subobject 安全。若 Derived assignment 先调用 Base::operator= 再复制成员,Item 12 还要求完整复制所有部分。

多重继承下地址可能有调整,不能用未经类型语义的 void* 比较替代稳健赋值。

self-move assignment

Buffer& Buffer::operator=(Buffer&& rhs) noexcept
{
    if (this != &rhs) {
        release();
        data_ = std::exchange(rhs.data_, nullptr);
        size_ = std::exchange(rhs.size_, 0);
    }
    return *this;
}

用户类型最好让 self-move 保持有效可析构状态;是否保持原值要文档化。guard 可保持值,swap-based move 也常自然安全。

copy-and-swap 的取舍

copy-and-swap 清晰但不总是最快:大对象可复用现有容量,成员逐项赋值可能更高效。优化版仍应先分配可能失败资源,再以 no-throw 操作提交。

Buffer& Buffer::assign(const Buffer& rhs)
{
    if (capacity_ < rhs.size_)
        return replace_with_candidate(rhs);
    copy_into_existing_storage(rhs); // 必须处理重叠或先检查身份
    size_ = rhs.size_;
    return *this;
}

必须用基准证明收益,并比 copy-and-swap 增加更多边界测试。

失败注入矩阵

先预测:delete-before-copy 在 self-assignment 时每一步的 image_ 指向什么;copy-before-delete 在 candidate 分配抛异常时目标值是什么。再运行 sanitizer 和资源计数。

  • a = a、两个 pointer/reference alias、Base alias 都保持合法值。
  • 子串 view 赋值先复制候选,不读取失效存储。
  • 每个候选分配点抛异常时目标满足承诺保证。
  • self-move 后对象可析构、可重新赋值且资源不重复释放。
  • 返回引用地址始终为 this。
  • 资源 create/destroy 计数在作用域结束后相等。

小结

  • self-assignment 包括直接同名,也包括 pointer、reference、base 和 subobject alias
  • delete-before-copy 会先摧毁源,并同时违反异常安全
  • identity test 修复直接 self-assignment,但不能单独提供异常保证
  • copy-before-delete 与 copy-and-swap 通过 prepare-then-commit 自然处理别名
  • rule of zero 优先消除手写资源 assignment
  • self-move 与跨类型 view 也要有明确有效状态和失败测试

资料与写作方式声明

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

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

名词解释

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

self-assignment

赋值左右最终引用同一对象的情况。

aliasing

不同表达式指向同一或重叠存储的关系。

dangling pointer

资源释放后仍保存其地址的指针。

use-after-free

访问生命周期已经结束存储的未定义行为。

identity test

比较 this 与 rhs 地址并在相同时返回的防护。

basic exception guarantee

失败后对象仍有效但值可能改变的保证。

defensive branch cost

为少见自赋值分支加入比较的常见路径成本。

self-assignment-safe by construction

无需特殊分支也能正确处理自赋值的设计。

copy-before-delete

先完整复制源候选再释放目标旧状态。

strong exception guarantee

失败后目标可观察值完全不变的保证。

copy-and-swap

候选复制后用 no-throw swap 提交的赋值习惯。

prepare-then-commit

分离可能失败准备与不抛状态替换的设计。

rule of zero

由资源安全成员组合且不手写特殊成员的设计。

independent value semantics

复制得到独立等价值的语义。

subobject aliasing

源 view/reference 指向目标内部表示的别名。

alias-safe candidate

修改目标前从别名源创建独立候选。

base-subobject alias

多个基类引用指向同一派生对象基类部分。

inheritance aliasing

不同继承路径引用同一最派生或重叠子对象。

self-move assignment

目标与右值源是同一对象的移动赋值。

generic self-move path

泛型算法因别名可能产生的 self-move。

transactional copy cost

构造完整候选以获得强保证的额外成本。

capacity-reuse assignment

保持保证时复用目标容量的优化赋值。

assignment failure injection

在复制各阶段主动抛异常验证状态的测试。

练习

  1. 问题 1:assignment to self 与 operator=。 修复 delete-before-copy,分别用 identity test、copy-before-delete 和 copy-and-swap 改写,比较保证。
  1. 问题 2:identity test 与 copy and swap。 Text 从 string_view 赋值,而 view 可能来自 Text 自身子串,设计安全实现。
  1. 问题 3:copy-swap 的 self-move 协议。 为独占 Buffer 设计 x = std::move(x) 的协议和测试。

讨论

评论区加载中…