深入探索C++内存模型(2)

构造函语意学是本书的第二章,这一章节主要针对构造函数在不同情况的底层实现细节进行的全面的讨论。阐述了在类对象(class object)的构造期间,编译器进行的额外操作的原理。针对默认构造函数(default constructor)和拷贝构造函数(copy constructor)分别展开了细致的讨论,引出了编译器 NRV 优化的概念,揭示了成员初始化列表的作用机理。

第二章 构造函数语意学 (the semantics of constructors)

2.1 default constructor 的构建操作

关键词 explicit 之所以被导入这个语言,就是为了提供给程序员一种方法,使他们能够制止“单一参数的 constructor ”被当作一个 conversion 运算符。

默认构造函数在需要的时候被编译器产生出来。但是这个需要有两层含义,一个是程序员的需要,另一个是编译器的需要。
只有在编译器需要的时候,才会合成默认构造函数(default constructor)。此外,被合成出来的 constructor 只执行编译器所需的行动。

带有 default constructor 的 member class object

如果一个 class 没有任何 constructor,但它内含一个有 default constructor 的 member object,那么这个 class 的 implicit default constructor 就是 “nontrivial” ,编译器需要为此 class 合成出一个 default constructor。不过这个合成操作只有在 constructor 真正需要被调用时才会发生。

带有 default constructor 的 base class

如果一个没有任何 constructor 的 class 派生自一个“带有 default constructor”的 base class,那么这个 derived class 的 default constructor 会被视为 nontrivial,因此需要被合成出来。它将调用上一层 base classes 的 default consturctor(根据他们的声明顺序)。对于一个后继派生的class而言,这个合成的 constructor 和一个 “被明确提供的 default constructor” 没有什么异常。

带有 virtual function 的 class

另有两种情况,也需要合成出 default constructor:

  1. class 声明(或继承)一个 virtual function;
  2. class 派生自一个继承串链,其中有一个或更多的 virtual base classes。

以下两个扩张会在编译期间发生:

  1. 一个 virtual function table(vtbl) 会被编译器产生出来,用于存放 class 的 virtual functions 地址;
  2. 在每一个 class object 中, 一个额外的 pointer member(vptr)会被编译器合成出来,内含相关的 class vtbl 的地址。

注意:虚函数表(vtbl)属于一个 class,而每个 object 中存放的是虚函数表的地址(vptr)。

带有 virtual base class 的 class

virtual base class 的实现方法在不同的编译器之间有极大的差异。然而,每一种实现方法的共通点在于必须使 virtual base class 在其每一个 derived class object 中的位置能够于执行期准备妥当。(书中举出了cfront编译器的做法:在derived class object 的每一个 virtual base classes 安插一个指针。)对于 class 所定义的每一个 constructor,编译器会安插那些“允许每一个 virtual base class 的执行期存取操作”的代码。如果 class 没有声明任何 constructors,编译器必须为它合成一个 default constructor。

总结

有四种情况,会导致“编译器必须为未声明 constructor 的 class 合成一个 default constructor”。C++ Standard 把那些合成物称为 implicit nontrivial default constructors。被合成出来的 constructor 只能满足编译器(而非程序)的需要。

在合成的 default constructor 中,只有 base class subobjects 和 member class objects 会被初始化,所有其他的 nonstatic data member,如整数、指针、数组等等都不会被初始化。这些初始化操作对程序而言或许有需要,但对编译器则并非必要。

C++新手一般有两个常见的误解:

  1. 任何 class 如果没有定义 default constructor,就会被合成出一个来;
  2. 编译器合成出来的 default constructor 会明确设定“class 内每一个 data member 的默认值”。

如你所见,没有一个是真的。

2.2 copy constructor 的构建操作

有三种情况,会以一个 object 的内容作为另一个 class object 的初值:

  1. 使用等号(“=”)明确地使用另一个object进行赋值
  2. object 被当作参数交给(传递给)某个函数
  3. 函数传回一个 class object (函数返回对象)

default memberwise initialization

如果 class 没有提供一个 explicit copy constructor 时,当 class object 以“相同 class 的另一个 object”作为初值时,其内部都是以所谓的 default memberwise initialization 手法完成的,也就是把每一个内建的或派生的 data member 的值,从某个 object 拷贝一份到另一个 object 身上,不过它并不会拷贝其中的 member class object,而是以递归的方式施行 memberwise initialization。

和以前一样,C++ Standard 把 copy constructor 区分为 trivial 和 nontrivial 两种,只有 nontrivial 的实体才会被合成于程序之中。而决定一个 copy constructor 是否为 trivial 的标准在于 class 是否展现初所谓的“bitwise copy semantics”。

bitwise copy semantics (位逐次拷贝)

一个 class 不展现出 “bitwise copy semantics” 的四种情况:

  1. 当 class 内含一个 member object 而后者的 class 声明有一个 copy constructor 时 (不论时被 class 设计者明确地声明,或是被编译器合成);
  2. 当 class 继承一个 base class, 而后者存在有一个 copy constructor 时(不论是被声明或是被合成而得);
  3. 当 class 声明了一个或多个 virtual function 时;
  4. 当 class 派生自一个继承串链,其中有一个或多个 virtual base class 时。

前面两种情况,编译器必须将 member 或 base class 的 copy constructor 调用操作安插到被合成的 copy constructor 中。而后面两种情况有点复杂,会在接下来的章节讨论。

重新设定 virtual table 的指针

虚函数表的相关初始化要在构造函数中完成,如果编译器对于新产生的 class object 的 vptr 不能正确地设置到其初值,将导致可怕的后果。因此,当编译器导入一个 vptr 到 class 之中时,该 class 就不再展现 bitwise semantics 了。现在,编译器需要需要合成出一个 copy constructor,以求将 vptr 适当地初始化。

当在拷贝构造时,如果发生切割(sliced)行为(使用派生类对象初始化基类对象),则需要注意 vptr 需要被设置为基类的虚函数表。

处理 virtual base class subobject

一个 class object 如果以另一个 object 作为初值,而后者有一个 virtual base class subobject, 那么也会使“bitwise copy semantics”失效。

每一个编译器对于虚拟继承的支持承诺,都表示必须让“derived class object 中的 virtual base class subobject 位置” 在执行期间就准备妥当,维护“位置的完整性”是编译器的责任。“bitwise copy semantics”可能会破坏这个位置,所以编译器必须在它自己合成出来的 copy constructor 中作出仲裁。

2.3 程序转化语意学(program transformation semantics)

在 class object 的初始化定义语句会经历两个阶段的程序转换:

  1. 重写定义语句,其中的初始化操作会被剥除,这里所谓的“定义”是指“占用内存”的行为;
  2. class 的 copy constructor 调用操作会被安插进去。

返回值的初始化(return value initialization)

编译器层面的优化操作,有时候被称为 named return value(NRV)优化。

考虑对如下定义函数定义进行优化。

X bar() {
  X xx;
  // ...
  return xx;
}

编译器把其中的 xx__result 取代:

void bar (X& __result) {
  // default constructor
  __result.X::X();
  // ... 直接操作 __result
  return;
}

虽然 NRV 优化提供了重要的效率改善,它还是饱受批评。有以下3点原因:

  1. 优化由编译器默默完成,而它是否真的被完成,并不十分清楚;
  2. 一旦函数变得比较复杂,优化也就变得比较难以施行;
  3. 某些情况下,程序员不喜欢应用程序被优化。

摘要

copy constructor 的应用,迫使编译器多多稍稍对你的程序代码做部分转化。尤其是当一个函数以传值(by value)的方式传回一个 class object,而该 class 有一个 copy constructor(不论时明确定义出来的,或是合成的)时。这将导致深奥的程序转化(不论在函数的定义或使用上)。
此外编译器也将 copy constructor 的调用操作优化,以一个额外的第一参数(数值被直接存放于其中)取代 NRV。

2.4 成员们的初始化队伍(member initialization list)

下列情况中,为了让你的程序能够被顺利编译,你必须使用 member initialization list:

  1. 当初始化一个 reference member 时;
  2. 当初始化一个 const member 时;
  3. 当调用一个 base class 的 constructor,而它拥有一组参数时;
  4. 当调用一个 member class 的 constructor, 而它拥有一组参数时。

注意成员变量在初始化过程中的顺序,初始化列表中的项目次序是由 class 中的 members 声明次序决定,不是由 initialization list 中的排列次序决定。

编译器会对 initialization list 依次处理并可能重新排序,以反映出 members 的声明次序。它会安插一些代码到 constructor 体内,并置于任何 explicit user code 之前。

(全文完)

Comments

Comments powered by Disqus