《深度探索C++对象模型》读书笔记[第二章]
2. 构造函数语义学
关键词 explicit 之所以被导入这个语言,就是为了提供给程序员一种方法,使他们能够制止 "单一参数的constructor"被单做一个 conversion运算符.[说人话就是explicit关键字的作用就是防止类构造函数的隐式自动转换]
2.1 Default Constructor 的建构操作
下面四小节讨论在需要的时候[编译器需要的时候]被编译器产生出来的Default Constructor的四种情况
2.1.1 "带有Default Constructor"的Member Class Object
如果一个class没有任何constructor,但它内含一个member object,而后者有default constructor, 那么这个class的immplicit default constructor就是有用的(书中用"nontrivial"),编译器需要为此class合成出一个default constructor.不过这个合成操作只有在constructor真正需要被调用时才会发生.
如果default constructor 已经被明确地定义出来,编译器没办法合成第二个,则编译器的行动是:"如果class A内含一个或一个以上的member class objects, 那么class A的每一个constructor 必须调用每一个 member classes的default constructor".编译器会扩张已存在的constructors,在其中安插一些代码,使得user code在被执行之前,先调用必要的default constructors.
如果有多个 class member objects 都要求 constructor 初始化操作, C++语言要求以 "member objects 在 class 中的声明次序"来调用各个 constructors.这一点由编译器完成,它为每一个 constructor 安插程序代码, 以 "member声明次序"调用每一个 member 所关联的 default constructors. 这些码被安插在 explicit user code 之前。
2.1.2 "带有 Default Constructor"的 Base Class
类似的道理,如果一个没有任何constructors的class派生自一个"带有 default constructor"的base class,那么这个derived class 的default constructor会被视为nontrivial,并因此需要被合成出来。它将调用上一层base classes的default constructor(根据他们的声明次序).对于一个后继派生的class而言,这个合成的consructor和一个"被明确提供的"default constructor"没有什么差异。
如果设计者提供多个constructors,但其中都没有default constructor,编译器会扩张现有的每一个constructors.【这种情况是否适用于2.1.1的情况?】
如果同时存在着"带有default constructors"的member class objects,那些default constructor也会被调用-在所有base class constructor都被调用之后。
2.1.3 "带有一个 VirtualFunction"的class
另有两种情况,也需要合成出 default constructor:
- class 声明(或继承)一个 virtual function.
- class 派生自一个继承串链,其中有一个或更多的 virtual base classes.
对于那些未声明任何 constructors的classes,编译器会为它们合成一个default constructor,以便正确地初始化每一个class object的 vptr.
2.1.4 "带有一个 Virtual Base Class" 的 Class
__vbcX(或编译器所做出的的某个什么东西)是在class object构建期间被完成的.对于class所定义的每一个constructor,编译器会安插那些"允许每一个 virtual base class的执行器存取操作"的码.如果class没有声明任何constructors,编译器必须为它合成一个 default constructor。
2.1.5 补充
不存在上述四种情况而又没有声明任何constructor的classes,他们的default constructors实际上并不会被合成出来。
2.2 Copy Constructor 的建构操作
有三种情况,会以一个 object 的内容作为另一个 class object 的初值:
- 对一个object做明确的初始化操作.
- 当object 被当做参数交给某个函数时.
- 函数返回一个 class object时.
2.2.1 Default Memberwise Initialization
如果class 没有明确提供 copy constructors, class object 以 "相同 class 的另一个 object" 作为初值时,其内部是以所谓的 default memberwise initialization 手法完成的,也就是把每一个内建的或派生的 data member(例如一个指针或一数目组)的值,从某个 object 拷贝一份到另一个 object身上.不过它并不会拷贝其中的member class object,而是以递归的方式施行 memberwise initialization。
决定一个 copy constructor 是否为trivial 的标准在于 calss 是否展现出所谓的 "bitwise copy semantics".
2.2.2 Bitwise Copy Semantics(位逐次拷贝)
如果存在class member存在copy constructor,则编译器必须合成出一个copy constructor,以便调用class member的copy constructor.被合成出来的copy constructor中,如整数,指针,数组等等的nonclass members也都会被复制,正如我们所期待的一样。
2.2.3 不要Bitwise Copy Semantics!
在下面四种情况下一个class不展现出 "bitwise copy semantics",即会合成出一个copy constructor. 四种情况如下:
- 当 class 内含一个 member object 而后者的 class 声明有一个 copy constructor时.(无论是被明确声明还是被合成而得)
- 当 class 继承自一个 base class 而后者存在有一个 copy constructor 时.(无论是被明确声明还是被合成而得)
- 当 class 声明了一个或多个 virtual functions时.[注意:与2.1.3第一种情况不同,这里没有继承的情况!!!]
- 当class 派生自一个继承串链,其中有一个或多个 virtual base classes时.
2.2.4 重新设定 Virtual Table 的指针
基类 ZooAnimal, 子类 Bear,如果有 Bear yogi; Bear winnie = yogi;的情况,合成出来的ZooAnimal copy constructor 会明确设定 object 的 vptr 指向 ZooAnimal class 的 virtual table,而不是字节从右手边的class object中将其 vptr现值拷贝过来。
[注:相同类型的初始化赋值可以靠位逐次拷贝完成,但是类型不同,发生截断的情况,编译器需要合成一个copy constructor会明确设定 object 的vptr.]
2.2.5 处理 Virtual Base Class Subobject
一个 virtual base class 的存在会使 bitwise copy semantics 无效.其次,问题不发生于"一个 class object 以另一个同类的 object 作为初值"之时,而是发生于 "一个 class object 以其 derived classes 的某个 object 作为初值"之时.当后者发生时,编译器必须合成一个 copy constructor,安插一些码以设定 virtual base class pointer/offet的初值(或只是简单地确定它没有被摸消),对每一个members执行必要的 memberwise 初始化操作, 以及执行其它的内存相关工作.
2.3 程序转化语意学(Program Transformation Semantics)
2.3.1 明确的初始化操作 (Explicit Initialization)
有定义 X x0;
如果有程序 void foo_bar() {
X x1( x0 );
X x2 = x0;
X x3 = X( x0 );
}
则编译器进行转化会有两个阶段:
-
重写每一个定义,其中的初始化操作会被剥夺.
-
class 的 copy constructor 调用操作会被安插进去.
可能的程序转化结果
void foo_bar() {
X x1; // 定义被重写,初始化操作被剥除
X x2; // 定义被重写,初始化操作被剥除
X x3; // 定义被重写,初始化操作被剥除// 编译器安插 X copy construction 的调用操作
x1.X::X( x0 );
x2.X::X( x0 );
x3.X::X( x0 );
}
2.3.2 参数的初始化 (Argument Initialization)
按值调用的函数有一种策略是导入所谓的暂时性object,并调用copy constructor将它初始化,然后将该暂时性object交给函数。
可以使用按引用调用来对付那个暂时性的 object
2.3.3 返回值的初始化(Rerutn Value Initialization)
已知下面的函数定义
X bar()
{
X xx;
// 处理 xx
return xx;
}
bar()的返回值如何从局部对象xx中拷贝出来?Stroustrup在cfront中的解决方法是一个双阶段转化:
1.首先加上一个额外参数,类型是 class object 的一个reference。这个参数将用来放置被“拷贝构建(copy constructed)” 而得的返回值
2.在return指令之前安插 一个 copy constructor调用操作,以便将欲传回之object的内容当作上述新增参数的初值。
// 函数转换
// 以反映出 copy constructor 的应用
// c++ 伪码
void bar(X& __result)
{
X xx;
// 编译器所产生的 default constructor 调用操作
xx.X::X();
// ... 处理 xx
// 编译器所产生的 copy constructor 调用操作
__result.X::XX{ xx };
return;
}
2.3.4 使用者层面做优化
在这个层面上, class 的设计是以效率考虑居多,而不是以"支持抽象化"为优先.
// 用
X bar(const T &y, const T &z)
{
return X(y,z);
}
// 替换
X bar(const T &y, const T &z)
{
X xx;
// ...以 y 和 z 来处理 xx
return xx;
}
当bar()的定义被转换之后,效率会比较高.
得到的伪码
// C++ 伪码
void bar(X &__result, const T &y, const T &z)
{
__result.X::X(y,z);
return;
}
__result 被直接计算出来,而不是经由 copy constructor 拷贝而得!
2.3.5 在编译器层面做优化
NRV优化
2.3.6 Copy Constructor:要还是不要?
不管hsiyong memcpy() 或 memset(), 都只有在 "classes 不含任何由编译器产生的内部 members "时才能有效运行.如果 Point3d class 声明一个或一个以上的 virtual functions,或内含一个 virtual base class,那么使用上述函数将会导致那些 "被编译器产生的内部 members" 的初值被改写.
2.4 成员们的初始化队伍(Member Initialization List)
下列情况,都必须使用member initialization list:
- 当初始化一个 reference member时;
- 当初始化一个 const membser时;
- 当调用一个 base class 的 constructor,而它拥有一组参数时;
- 当调用一个 member class 的 constructor,而它拥有一组参数时.
list中的项目次序是由 class 中的 members 声明次序决定,不是由 initialization list 中的排列次序决定。
initialzation list的项目被放在 explicit user code之前
Member function的使用是合法的,这是因为和此 object 相关的 this 指针已经被构建妥当.
总结:
编译器会对 initialization list 一一处理并可能重新排序,以反映出 members
的声明次序,它会按差一些代码到 constructor 体内,并置于任何 explicit user code 之前。