Item 5:了解 C++ 默默编写并调用哪些函数

对齐 Effective C++ 第三版 Item 5:推导编译器隐式声明、定义或删除的默认构造、析构、复制与现代移动操作,并从 base/member 语义验证生成结果。

学习目标

  • 能推导一个类的 default constructor、destructor、copy constructor、copy assignment 与现代 move 操作是否隐式声明、定义或删除
  • 能分析编译器生成操作如何逐个调用 base/member 对应函数,识别 raw pointer、const、reference 与 inaccessible 子操作的影响
  • 能设计符合值、独占所有者、多态基类或身份对象语义的特殊成员集合,并以 type traits 和资源测试验证
Special member generation map隐式声明 → 子对象传播 → 可调用性与真实行为functions c++ silently writesimplicit declarationsix member decisionsdefault constructorbase + membersinvariant at birthcopy constructorinitialize membersconst / reference okaycompiler-generated functionsmemberwise contractdefault / deleted / customcopy assignmentconst / reference may deletedestructorreverse cleanupsubobject abilitydeleted propagates outwardevidencetraits + resource counttraits 证明“能否调用”,计数与失败注入证明“实际怎么做”
特殊成员不是隐藏魔法,而是由每个 base/member 的操作能力组合出来的接口契约。

特殊成员推导实验

先预测:这个操作会生成、删除还是调用复制?

先预测 subobject 传播结果,再切换场景查看 traits 与运行时证据。

观察

空 class 可能获得默认构造、析构、复制构造和复制赋值;现代 C++ 还要审查 move。隐式声明不等于可调用。

决策

先列出六个特殊成员,再逐项追踪 base 与 member 的对应操作,最后决定 default、delete 或自定义。

当前场景 · functions c++ silently writes / compiler-generated functions

先列出六个特殊成员,再逐项追踪 base 与 member 的对应操作,最后决定 default、delete 或自定义。

从“空 class 并不是什么都没有”开始

class Empty {};

源码没有声明函数,但在被需要且满足规则时,编译器可能为它声明并定义默认构造、析构、复制构造和复制赋值。原书 Item 5 要求先知道

否则接口会在没有显式设计的情况下获得复制、赋值或销毁能力。 Know what functions C++ silently writes and calls(了解 C++ 默默编写并调用哪些函数)要求把这些隐式能力当成真实 public 接口审查,而不是把源码中“没写”误认为类型“不支持”。

现代 C++ 还包括 move constructor 和 move assignment,因此应在原书四项基础上审查六项。

隐式声明不等于一定可调用

要区分。某操作可在重载集合出现,却因成员不可复制而被定义为 deleted。

struct Record {
    const int id;
    std::string name;
};
 
static_assert(std::is_copy_constructible_v<Record>);
static_assert(!std::is_copy_assignable_v<Record>);

copy constructor 可直接初始化 id,copy assignment 却不能重新赋值 const member。

现代 = delete 能显式表达相同结果,Item 6 将专门处理不想要的生成函数。

default constructor 何时出现

只有类没有用户声明任何构造函数时,编译器才隐式声明默认构造。声明一个带参数构造并不会自动再送一个无参版本。

class Endpoint {
public:
    explicit Endpoint(std::string address)
        : address_{std::move(address)} {}
private:
    std::string address_;
};
 
static_assert(!std::is_default_constructible_v<Endpoint>);

隐式默认构造会依次默认初始化基类和成员;若某成员没有默认构造,整体默认构造被删除。

即使写 T() = default 也属于显式决定接口,而不是“编译器没管”。

destructor 不会自动变 virtual

隐式析构会调用每个成员与基类析构,但不会因为类有 virtual 函数就自动成为 virtual。

struct Interface {
    virtual void run() = 0;
    // ~Interface() 非 virtual,若经 Interface* delete 派生对象会出错
};

必须由设计者声明。Item 7 会深入这一约束。

隐式 copy 是 memberwise copy

默认实现不是逐字节 memcpy,而是先复制基类,再按声明顺序对成员执行对应复制操作。

struct NamedCounter : CounterBase {
    std::string name;
    std::array<int, 4> buckets;
    Observer* observer;
};

生成 copy constructor 调用 CounterBase copy constructor、std::stringstd::array copy constructor,并复制 observer 地址。它不知道 observer 是借用还是所有权。

raw pointer 被浅复制不是编译器 bug,而是类型没有表达资源语义。

子对象能力决定整体能力

若任何必要 subobject 操作被删除、不可访问或有歧义,外层对应特殊成员也不可用。

class NonCopyable {
public:
    NonCopyable() = default;
    NonCopyable(const NonCopyable&) = delete;
};
 
struct Service {
    NonCopyable token;
};
 
static_assert(!std::is_copy_constructible_v<Service>);

编译器不会绕过成员契约。组合自然传播不可复制、不可默认构造等能力。

引用成员阻断赋值

copy construction 可以让新引用绑定源引用所指对象;copy assignment 无法决定是修改所指对象还是重新绑定,因此生成赋值通常 deleted。

struct View {
    Buffer& buffer;
    std::size_t offset{};
};
 
static_assert(std::is_copy_constructible_v<View>);
static_assert(!std::is_copy_assignable_v<View>);

这可能正是 view 类型语义,也可能说明 reference member 不适合。不能只为让 trait 变 true 而写含糊赋值。

现代 move 生成规则

通常只有类没有用户声明 copy constructor、copy assignment、move constructor、move assignment 和 destructor 时,编译器才隐式声明 move 操作。

struct Payload {
    std::vector<std::byte> bytes;
};
 
static_assert(std::is_move_constructible_v<Payload>);
static_assert(std::is_nothrow_move_constructible_v<Payload>);

尤其“只写一个空析构”可能让容器重分配回落到 copy。原书年代没有 move,但理解“编译器默默写什么”在现代必须包含这一变化。

move 可能调用 copy

“类型 is_move_constructible 为 true”不一定表示发生廉价资源转移,因为 copy constructor 也可接受右值。

struct CopyOnly {
    CopyOnly() = default;
    CopyOnly(const CopyOnly&) {}
};
 
static_assert(std::is_move_constructible_v<CopyOnly>); // 可由 copy 接受右值

需要用计数器或性能测试确认行为,不只看 trait 名称。noexcept 也影响 vector 是否愿意移动元素。

只有真实不抛时才声明 noexcept。

rule of zero 优先

class Report {
public:
    Report(std::string title, std::vector<Row> rows)
        : title_{std::move(title)}, rows_{std::move(rows)} {}
private:
    std::string title_;
    std::vector<Row> rows_;
};

string/vector 已定义正确复制、移动和析构,Report 的 compiler-generated functions 组合出正确值语义。

让编译器生成函数成为可依赖工具,而非风险。

何时需要 rule of five

不是要求五个都手写,而是五个都做决定:= default= delete 或自定义。独占 owner 通常删除 copy、默认/自定义 move;共享资源先明确复制是共享还是深拷贝;身份对象可能全部删除。

对它禁止复制比伪造复制语义更正确。

编译期与运行期双重验证

static_assert(std::is_default_constructible_v<T> == expectedDefault);
static_assert(std::is_copy_constructible_v<T> == expectedCopy);
static_assert(std::is_copy_assignable_v<T> == expectedCopyAssign);
static_assert(std::is_move_constructible_v<T> == expectedMove);
static_assert(std::is_nothrow_move_constructible_v<T> == expectedNoThrowMove);

traits 证明接口可用性,不证明深拷贝、单次释放或 moved-from 行为。运行期用 InstrumentedMember 统计每个 base/member 的 ctor/copy/move/assign/dtor,并注入异常。

先预测:给一个 rule-of-zero 类添加空 destructor 后,哪些 traits 仍可能为 true,真实 move 是否会回落 copy?运行 vector 重分配计数测试验证。

  • 默认构造后所有成员不变量成立。
  • copy 后修改目标不影响源,或按文档共享。
  • self-assignment 与异常路径保持合法状态。
  • move 后目标拥有资源,源可析构和重新赋值。
  • 每个资源在所有对象销毁后恰好释放一次。

小结

  • 空 class 也可能拥有 compiler-generated default ctor、destructor、copy ctor 和 copy assignment
  • 现代 C++ 还需审查 move ctor/assignment,用户声明 copy/move/destructor 会影响生成
  • 隐式操作逐个调用 base/member 对应操作,raw pointer 只复制地址
  • const/reference 或 deleted/inaccessible 子操作会让整体操作不可用
  • rule of zero 让资源语义由成员组合;直接管理资源时系统决定 rule of five
  • type traits 只证明可调用性,资源与性能语义还需运行期计数和失败注入

资料与写作方式声明

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

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

名词解释

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

compiler-generated functions

编译器按规则隐式声明或定义的对象特殊操作。

特殊成员函数

控制对象创建复制移动与销毁的成员函数。

implicit declaration

编译器把特殊成员签名加入类接口的阶段。

implicit definition

操作被需要时按子对象生成函数体的阶段。

deleted function

被选择调用就编译失败的函数定义状态。

default constructor

无需实参即可构造对象的函数。

user-declared constructor

程序员显式声明并抑制隐式默认构造的构造函数。

destructor

生命周期结束时逆序清理子对象的函数。

virtual destructor

支持经基类指针完成派生析构的析构函数。

copy constructor

从同类型源初始化新对象的特殊成员。

copy assignment

把同类型源值复制到现有目标的特殊成员。

memberwise copy

对每个 base/member 执行对应复制操作的语义。

subobject

对象内部的基类、成员或数组元素对象。

inaccessible subobject operation

生成外层操作时无法调用的子对象函数。

reference member

构造时绑定且之后不可重新绑定的成员引用。

move constructor

从右值源转移资源构造新对象的函数。

move assignment

从右值源转移资源到现有目标的函数。

move suppression

用户声明相关函数后不再隐式生成 move 的规则。

move fallback to copy

右值在缺少 move 时由 copy const reference 接受。

noexcept move

承诺不抛并让容器可安全选择移动的操作。

rule of zero

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

value semantics

复制得独立等价值、移动转移表示的对象语义。

rule of five

直接管理资源时系统决定五个复制移动析构操作。

identity object

不应被表示为独立可复制值的身份绑定对象。

type traits audit

编译期查询类型构造复制移动能力的审计。

练习

  1. 问题 1:functions c++ silently writes 与 compiler-generated functions。 类含 const id、reference view、vector data 和 user-declared destructor,逐项判断六个特殊成员。
  1. 问题 2:default constructor 与 destructor。 一个类以 raw pointer 拥有数组并使用默认 copy,设计 rule-of-zero 与 rule-of-five 两种修复。
  1. 问题 3:copy constructor 与 copy assignment。 给 rule-of-zero 类型添加日志析构后,设计实验判断 vector 扩容为何从 move 变 copy。

讨论

评论区加载中…