第2章:构造函数语义

对齐原书第2章 The Semantics of Constructors:默认构造与拷贝构造的合成条件、程序变换、返回值优化和成员初始化列表。

学习目标

  • 能判断 compiler 何时必须为 member、base、virtual function 与 virtual base 生成 default construction 工作
  • 能比较 bitwise copy semantics 与 nontrivial memberwise initialization,并推导 vptr、virtual base 对 copy constructor 的影响
  • 能分析 explicit、argument、return value initialization 与 member initialization list 的程序变换和优化边界

从“编译器暗中做了什么”开始

constructor 的隐藏动作不是任意魔法,而是语言语义要求的程序变换。先预测下面空 constructor 是否真的“什么也不做”:若 class 含有需要默认构造的 member/base,或 implementation 必须建立 virtual representation,即使 user 看不到语句,生成代码仍有职责。

需要区分三个层次:函数被 implicitly declared、函数被 odr-used 后需要 definition、该 definition 是 trivial 还是 nontrivial。口语里“编译器给每个类一个默认构造函数”太粗糙:有些类没有可调用的 default constructor,有些隐式 default constructor 不做 observable work,有些则必须调用 subobject constructor 或设置实现元数据。

2.1 Default Constructor Construction

Member Class Object with Default Constructor

Widget 含有 class-type member Name name_,而 Name 需要 default constructor,compiler 形成的 Widget::Widget() 必须调用它。调用顺序按 member declaration order,不按 constructor body 中第一次使用的顺序。

struct Name {
    Name();
};
 
struct Widget {
    Name name;
    int count;
};
 
Widget widget; // name 被构造;count 的值取决于初始化形式,不因合成构造而一律清零

compiler 只补语义要求的工作,不替 programmer 猜业务初值。int count 没有 default member initializer 时,default-initialization 与 value-initialization 的结果还要按具体语境判断。安全设计应显式写 int count = 0;,不要依赖“对象看起来刚分配”的偶然 bytes。

Base Class with Default Constructor

derived constructor 在进入自己的 member initialization 与 body 前,必须完成 direct base construction。多个 direct bases 按 base-specifier list 顺序;随后 members 按 declaration order。若 required base default constructor 不可访问或被删除,derived default constructor 也可能被定义为 deleted。

Class with a Virtual Function

polymorphic object 在 construction 的每个 subobject phase 都必须表现为当前正在构造的 class,而不是尚未完成的 most-derived class。常见 implementation 会设置适合当前 phase 的 vptr,使 constructor 中的 virtual call 不越过尚未建立的 derived invariant。

这不是“constructor 开始时一次写好最终 vptr”那么简单。base constructor 运行时要呈现 base virtual behavior;进入 derived construction 后再切换为 derived representation。第 5 章会完整展开这条序列。

Class with a Virtual Base Class

virtual base 在 most-derived object 中只有一个共享 subobject,因此应由 most-derived constructor 负责构造。中间 base 可以在 initializer list 写 virtual-base initializer,但当它作为更大 derived object 的 subobject 构造时,那份请求不会重复构造 shared virtual base。

2.2 Copy Constructor Construction

Default Memberwise Initialization

没有 user-declared copy constructor 时,隐式 copy 的语义是按顺序复制 direct bases 和 nonstatic members。对 scalar member 是值复制;对 class member 则调用该 member 的 copy constructor。所谓 memberwise 是语义描述,不意味着生成代码一定逐字段,也不意味着一定调用 memcpy

Bitwise Copy Semantics

对足够简单、trivially copyable 的对象,compiler 可以把 memberwise result lower 成 block copy 或寄存器 load/store。关键是“结果等价”,不是源码层授予任意对象 memcpy 权限。owner pointer、self-reference、mutex、virtual base 等都可能让 raw bytes 不表达正确独立对象。

struct Plain {
    int id;
    double weight;
};
 
struct Record {
    std::string name;
    Plain payload;
};
 
Plain a{1, 2.0};
Plain b = a;   // implementation 可用位级等价复制
Record r2 = r1; // 必须满足 string 的 copy semantics

Bitwise Copy Semantics - Not:Resetting the Virtual Table Pointer

以下条件会要求 nontrivial copy work:base/member 有 nontrivial copy constructor;class 使用 virtual function,需要让 target object 保持正确 vptr;class 含 virtual base,需要按 most-derived 结构处理共享 subobject。原书把前者称为 resetting the virtual table pointer,把后者称为 handling the virtual base class subobject。user-declared copy constructor 则直接把复制 policy 交给 user body,并仍由语言安排 bases/members 的初始化入口。

不能把 source object 的 vptr 当普通业务 bytes 无脑复制。source 可能通过 base view 参与 copy,而 target 的动态身份由正在构造的 class 决定;implementation 要建立 target 应有的 virtual table pointer。virtual base 也必须避免沿多条路径重复复制。

2.3 Program Transformation Semantics

Explicit Initialization

X target(source)X target = source 都可能选择 copy constructor;后者的语法不能据此理解为先 default construct 再 assignment。initialization 建立新 lifetime,assignment 修改已经存在的 object,两者必须分开推导。

Argument Initialization

by-value parameter 是被初始化的 parameter object。概念上 caller 提供 initializer,callee 使用独立 object;ABI 可决定由谁提供 storage、是否通过 registers 传递,language/optimizer 也可能消除不必要 temporary。若只需观察且不需要独立 value,const X& 可改变接口语义和 copy 成本,但也引入 alias/lifetime 约束。

Return Value Initialization

原书重点讨论 compiler 如何把 return object 写入 caller 提供的 hidden result storage,以及 named return value optimization。现代 C++ 进一步规定某些 prvalue 情形直接构造最终对象,不再把“临时对象复制后再优化掉”作为正确心智模型。

Widget makeWidget() {
    Widget result;
    return result; // NRVO 可能成立
}
 
Widget makeDirect() {
    return Widget{}; // 现代规则下可直接构造返回目标
}
 
Widget first = makeWidget();
Widget second = makeDirect();

user-level optimization 可以改变 API,例如返回值改输出参数或避免不必要 by-value parameter;但现代 compiler 的 elision 往往已经让自然 return-by-value 更清晰且同样高效。优化前先测量,并检查改变后是否扩大 alias、破坏 exception safety 或让 ownership 更模糊。

The Copy Constructor: To Have or To Have Not

只有当 class 的 copy policy 不是正确 memberwise copy 时才手写 copy constructor,例如 deep copy、reference rebinding policy 或禁止复制。无必要的 user-declared copy 会让类型失去 trivial property、阻碍 move generation 或增加 maintenance。现代代码优先 Rule of Zero:让 member owners 自己表达资源语义。

2.4 Member Initialization List

initializer list 能直接构造 const member、reference member、无 default constructor 的 member,并避免“先默认构造、再 assignment”。但它的书写顺序不控制执行顺序。

class BufferView {
public:
    BufferView(std::size_t size, const std::byte* data)
        : size_(size), end_(data + size_), begin_(data) {}
 
private:
    const std::size_t size_;
    const std::byte* begin_;
    const std::byte* end_;
};

实际顺序是 size_begin_end_,因为 declaration order 如此;list 把 end_ 写在 begin_ 前也不能改变。示例中 end_ 只依赖 parameter data 与已完成的 size_,所以结果可推导。若 initializer 依赖一个声明在自己之后的 member,就会读取尚未初始化的状态。

第2章验证清单

  1. 区分 implicit declaration、需要 definition 与 nontrivial generated work。
  2. 标出 member class object、base、virtual function、virtual base 各触发什么 default construction。
  3. 明确 scalar members 是否有 initializer,不假设 synthesis 自动清零。
  4. 对 copy path 逐个检查 base/member 的 copy semantics。
  5. 判断 vptr 与 virtual base 是否阻止 naive bitwise copy。
  6. 分开 explicit initialization、argument initialization、return value initialization 与 assignment。
  7. 在当前 C++ standard mode 下检查 mandatory elision、optional NRVO 和 ABI lowering。
  8. 让 member initializer list 与 declaration order 对齐,并检查依赖方向。

小结

  • constructor synthesis 只生成语言语义要求的工作,不承诺初始化所有业务状态
  • member class object、base、virtual representation 和 virtual base 会让 default construction 变得 nontrivial
  • default memberwise initialization 是逐 subobject 的语义,bitwise copy 只是可能的等价 lowering
  • nontrivial member/base、vptr 修复与 virtual base 处理会阻止无条件 raw byte copy
  • explicit、argument 和 return value initialization 都在建立新对象,不应与 assignment 混淆
  • NRVO 是具名返回对象的允许优化,现代 prvalue 还存在保证 copy elision 的情形
  • 无必要手写 copy constructor 会损失 triviality 并增加特殊成员函数耦合
  • member initialization list 直接构造 subobject,但实际顺序由 base/member declaration 决定

名词解释

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

constructor synthesis

编译器按需形成特殊成员函数定义。

default constructor construction

无实参建立对象的默认构造语义。

memberwise default initialization

按语言顺序默认初始化各 subobject。

vptr initialization

建立当前 construction phase 的虚表指针状态。

virtual base subobject

most-derived object 中唯一共享的虚基类实例。

copy constructor construction

用同类型对象建立新对象的复制构造语义。

default memberwise initialization

按 base/member 应有语义逐个复制。

bitwise copy semantics

对象表示允许等价字节复制的条件。

program transformation semantics

把初始化映射为构造目标与隐藏存储的编译器变换。

named return value optimization

具名局部返回对象直接构造到结果存储。

member initialization list

在 body 前直接构造 base 和 member 的语法。

练习

  1. 问题 1:判定隐式默认构造的工作。 给一个含 std::string member、polymorphic base、virtual base 和未初始化 int 的 derived class,列出 compiler 必须安排的动作与不会替你做的动作。
  1. 问题 2:判断能否位拷贝。 分别分析只含两个整数的 Plain、含 std::stringRecord、含 virtual function 的 Shape、含 virtual base 的 Node
  1. 问题 3:审计一次返回值与初始化列表。 对具名局部返回、直接 prvalue 返回、by-value 参数和依赖后声明 member 的 initializer,分别写出语义对象、可选优化和潜在错误。

讨论

评论区加载中…