Item 12:复制对象的所有组成部分

对齐 Effective C++ 第三版 Item 12:让 copy constructor/assignment 复制所有 base/member 与资源语义,防止新增字段和派生基类状态遗漏,并区分构造与赋值生命周期。

学习目标

  • 能绘制类型的 base/member/resource/cached-state 复制覆盖矩阵,识别新增字段与派生基类部分的遗漏
  • 能实现 Derived copy constructor 与 copy assignment,分别调用 Base copy/assignment 并保持独立值或共享语义
  • 能比较构造和赋值生命周期,使用 rule of zero、defaulted operations 或阶段无关 helper 消除错误重复
Complete copy coverage先覆盖对象模型,再选择每一项的独立、共享或失效语义copy all parts ofan objectcoverage matrixcopy constructorbuild new lifetimeBase{rhs} + memberscopy assignmentreplace old stateBase::operator=base class componentsresource / cachenew data memberdeep copy独立资源表示shared copy明确共享协议sentinel state test拒绝默认值假通过rule of zero结构演进自动覆盖构造负责建立完整生命周期,赋值负责替换已有状态;两条路径都不能遗漏 base/member
复制覆盖矩阵把 base、member、资源与缓存显式列出,避免新增字段或继承层悄悄掉出 copying functions。

完整复制覆盖实验

先预测:哪一项 copy 会被遗漏?

先区分构造与赋值的生命周期,再切换覆盖矩阵查看 sentinel 证据。

观察

完整对象不只有当前类字段,还包括 base class components、资源所有权与 derived cache state;遗漏一项也可能保留默认值或旧状态。

决策

先画 copy coverage matrix,再为每个 base/member/resource 选择复制、失效、共享或禁止复制的语义;不要让 operator== 成为唯一 oracle。

当前场景 · copy all parts of an object

先画 copy coverage matrix,再为每个 base/member/resource 选择复制、失效、共享或禁止复制的语义;不要让 operator== 成为唯一 oracle。

从“编译器不警告遗漏的新成员”开始

class Customer {
public:
    Customer(const Customer& rhs)
        : name_{rhs.name_} {}
 
    Customer& operator=(const Customer& rhs)
    {
        name_ = rhs.name_;
        return *this;
    }
private:
    std::string name_;
    Date lastTransaction_; // 后来新增,却未加入两个复制函数
};

手写 copy functions 后,编译器通常不会提醒 lastTransaction_ 被遗漏,因为成员本身可默认构造/保持旧值。

Item 12 要求 Copy all parts of an object(复制对象的所有组成部分)。

让代码演进时遗漏变成可审计事项。

copy constructor 覆盖所有初始状态

Customer::Customer(const Customer& rhs)
    : name_{rhs.name_},
      lastTransaction_{rhs.lastTransaction_}
{}

成员按声明顺序构造。若缓存只由业务值派生,可选择复制缓存与有效标记,或把缓存设为 invalid 以后重算。

处理策略必须保持逻辑 constness 和性能预期,而不是随意漏掉。

copy assignment 覆盖所有现有状态

Customer& Customer::operator=(const Customer& rhs)
{
    if (this != &rhs) {
        name_ = rhs.name_;
        lastTransaction_ = rhs.lastTransaction_;
    }
    return *this;
}

目标已经有合法值和资源;赋值必须释放、复用或替换旧状态,并满足 Item 10 返回契约与 Item 11 alias/exception 保证。

copy constructor 没有这一问题,因此二者生命周期不同。

派生 copy 必须复制 Base 部分

class PriorityCustomer : public Customer {
public:
    PriorityCustomer(const PriorityCustomer& rhs)
        : Customer{rhs}, priority_{rhs.priority_} {}
 
    PriorityCustomer& operator=(const PriorityCustomer& rhs)
    {
        if (this != &rhs) {
            Customer::operator=(rhs);
            priority_ = rhs.priority_;
        }
        return *this;
    }
private:
    int priority_{};
};

若 copy constructor 不写 Customer{rhs},Base 会 default construct,源的 name/date 丢失。若 assignment 不显式调用 Base::operator=,目标 Base 保留旧值。

不是虚分派;每层负责自己声明的状态并调用直接基类。

多层继承逐层负责

MostDerived copy
  -> DirectBase copy
      -> RootBase copy
  -> MostDerived members

虚基类由最派生 copy constructor 负责初始化,规则比简单链更复杂。

设计 copy 时必须按真实对象模型检查,而非只看当前类成员列表。

资源“复制”先定义语义

raw pointer 的地址复制无法表达选择。

class Image {
public:
    Image(const Image& rhs)
        : pixels_{std::make_unique<Pixel[]>(rhs.size_)}, size_{rhs.size_}
    {
        std::copy_n(rhs.pixels_.get(), size_, pixels_.get());
    }
private:
    std::unique_ptr<Pixel[]> pixels_;
    std::size_t size_{};
};

若 Image 是 value object,应 deep copy;若多个对象共享 immutable texture,可用 shared_ptr 明确 shared copy。

先设计再编码。

不要让 copy constructor 调 assignment

Customer::Customer(const Customer& rhs)
{
    *this = rhs; // 错误方向:成员先默认构造,再赋值
}

这里所有成员进入 constructor body 前已初始化,调用 assignment 产生 default-then-assign;const/reference member 无法处理;assignment 还可能假设目标旧资源存在。

不能用少写几行代码换生命周期混淆。

不要让 assignment 直接调用 constructor

C++ 不能在现有对象上普通调用 constructor 来“重建”而不先结束生命周期。placement new 手动重建会影响引用、const、继承和异常安全,普通 operator= 不应这样做。

这不是常规复制赋值;只有容器/variant 等底层实现并严格遵守生命周期规则时使用。

Customer& Customer::operator=(const Customer& rhs)
{
    this->~Customer();
    new (this) Customer(rhs); // 异常后 this 不再是有效 Customer
    return *this;
}

copy construction 抛异常时目标生命周期未恢复,operator= 连 basic guarantee 都没有。

安全共享逻辑的方法

struct CustomerData {
    std::string name;
    Date lastTransaction;
};
 
CustomerData copyData(const Customer& source);

copy constructor 用 helper 结果初始化,assignment 用 helper 结果 prepare-then-commit。更常见是把相关成员聚合成正确值类型,让 compiler-generated copy 自动覆盖。

class Customer {
public:
    Customer(const Customer&) = default;
    Customer& operator=(const Customer&) = default;
private:
    std::string name_;
    Date lastTransaction_;
};

新增成员会自动进入 memberwise copy,前提是每个成员的复制语义正是所需语义。

rule of zero 让覆盖随结构演进

比手写 default 更少维护。若类需要自定义 copy,说明有资源、缓存、身份或不变量的特殊语义,应把原因写入覆盖矩阵。

自动覆盖不等于自动业务正确:新增 mutex 会删除 copy,新增 shared_ptr 会引入共享,编译或测试应促使重新审查。

move 也要覆盖所有组成部分

原书聚焦 copying functions;现代同样审查 move constructor/assignment。

PriorityCustomer::PriorityCustomer(PriorityCustomer&& rhs) noexcept
    : Customer{std::move(rhs)},
      priority_{std::exchange(rhs.priority_, 0)}
{}

若漏掉 Base move,Base 可能 default construct 或 copy,性能与语义变化。noexcept 必须覆盖所有实际子操作。

不能只清当前类成员而让 Base 保持矛盾状态。

复制验证不是只比较 operator==

operator== 本身也可能漏同一成员,不能让生产 equality 成为唯一 oracle。测试可通过公共观察接口或专用 test builder 独立检查。

例如序列化全部业务字段、逐 getter 比较、资源身份计数和修改独立性。

异常路径逐成员注入

copy constructor 失败时对象从未完成构造,但已完成的 base/member 会自动析构;assignment 则必须满足 basic 或 strong guarantee。

先预测:Derived copy constructor 漏掉 Base{rhs} 时,Base sentinel 会变成什么;assignment 漏掉 Base::operator= 时目标 Base 保留哪个值。运行覆盖矩阵验证。

  • 所有 base/member 使用不同 sentinel,copy 后逐项等价。
  • deep resource 修改目标不影响源;shared resource 引用计数符合协议。
  • assignment 从完全不同旧状态开始,旧资源释放一次。
  • 第 N 个成员抛异常时无泄漏,目标满足声明保证。
  • 新增字段的 mutation test 能让旧 copy 测试失败。
  • move 后目标完整、源 whole-object invariant 成立。

小结

  • 自定义 copying functions 必须复制所有 base/member 与资源语义
  • 新增成员不会自动提醒手写 copy,需覆盖矩阵和 sentinel test
  • Derived copy constructor/assignment 分别调用 Base copy/assignment
  • copy constructor 建立新生命周期,assignment 修改已有对象,不能互相调用
  • rule of zero/defaulted operations 让 memberwise copy 随结构演进
  • 现代 move 同样要覆盖全部组成并维护 whole-object moved-from invariant

资料与写作方式声明

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

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

名词解释

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

complete copy contract

复制后所有组成等价并符合资源语义的要求。

copy coverage matrix

列出 base/member/resource 在复制路径处理方式的矩阵。

copy constructor

从源直接初始化新对象全部子对象的函数。

derived cache state

可从主状态重新计算、不属于抽象值的缓存。

copy assignment

把源等价值提交到已有目标并处理旧资源的函数。

pre-existing target state

赋值前目标已经拥有且需替换的合法状态。

base subobject

派生对象内部由基类提供的对象部分。

base-copy delegation

派生复制明确调用直接基类对应复制操作。

layer-by-layer copy

每层复制自身并委托直接基类的责任。

virtual base copy

由最派生 copy constructor 负责的虚基类复制。

deep copy

为目标创建独立资源表示的复制策略。

shared copy
复制后共同拥有同一资源的策略。
resource copy semantics

资源复制是独立、共享或禁止的含义。

under-construction target

成员和基类生命周期仍在建立的目标。

construction-via-assignment

错误用赋值实现对象构造的设计。

in-place reconstruction

结束对象后在同一存储 placement-new 的操作。

phase-independent copy helper

只从源生成候选、不依赖目标阶段的 helper。

defaulted copy operation

以 = default 请求编译器 memberwise copy。

rule of zero

由正确成员组合且不声明特殊成员的设计。

structural copy evolution

默认复制随成员结构变化自动更新的性质。

complete move contract

移动全部 base/member 并保持源有效的责任。

whole-object moved-from invariant

移动后源全部组成共同满足的最小不变量。

sentinel state test

为每项设置非默认值以暴露复制遗漏的测试。

independent copy oracle

不复用被测实现而构造复制预期的方法。

member-copy failure injection

让第 N 个子对象复制失败验证清理的测试。

练习

  1. 问题 1:copy all parts of an object 与 base class components。 Derived 自定义 copy 只复制自己的成员,补齐 Base、缓存和资源策略。
  1. 问题 2:copy constructor 与 copy assignment。 copy ctor 与 assignment 有相同字段转换逻辑,说明哪些共享方式安全、哪些不安全。
  1. 问题 3:new data member 的回归测试。 设计一个不会与生产 operator== 共同遗漏字段的复制测试。

讨论

评论区加载中…