zhouqijie

构造函数的语义

implicit:隐式的。
explicit:显式的。
trivial:没有用的。
non-trivial:有用的。
memberwise:逐成员施以。
bitwise:逐位施以。
semantic:语义。





默认构造函数的构造操作



带有默认构造函数的成员类对象

如果一个class没有任何构造函数,但它内含一个成员对象,而后者有默认构造函数,那么这个类的隐式构造函数就是”nontrivial”,编译器需要为该class合成出一个默认构造函数。(不过这个合成操作只有在构造函数真正需要被调用时才会发生)

CRE:构造成员对象是编译器的责任,但其他成员字段需要用户初始化。

如果默认构造函数被用户显式地定义出来了,编译器没法合成第二个。编译器的做法是:如果class内含一个以上的成员类对象,那么class的每一个构造函数必须调用每一个成员类对象的默认构造函数。编译器会扩展已存在的构造函数,在其中安插一些代码,使得用户代码执行之前,先调用必要的默认构造器们(default constructors)。



带有默认构造函数的基类

类似地,如果一个没有任何构造函数的类派生自一个带有默认构造函数的基类,那么这个派生类的默认函数会被视为”nontrivial”,并因此需要被合成出来。

编译器同样也会扩展现有的构造函数,将“用以调用所有必要的默认构造函数”的程序代码加进去。



带有一个虚函数的类

另有两种情况,也需要合成出默认构造函数:

  1. class声明或继承一个虚函数。
  2. class派生自一个继承链,其中有一个或以上的virtual基类。

下面两个扩张行为会在编译期间发生:

  1. 一个虚函数表会被编译器产生出来,内放class的虚函数地址。
  2. 在每一个类对象中,一个额外的指针成员vptr会被编译器合成出来,内含相关的虚函数表地址。

为了让这个机制发挥功效,编译器必须为每一个对象的vptr设定初值,放置合适的虚函数表地址。对于class所定义的每一个构造函数,编译器会安插一些代码来做这样的事情。对于那些未声明任何构造函数的类,编译器会为它们合成一个默认构造函数,以便正确地初始化每一个类对象的vptr。



带有一个虚基类的类

虚基类的实现法在不同编译器之间有着极大的差异。然而,每一种实现法的共同点在于必须使虚基类在其每一个派生类对象中的位置,能够在执行期准备妥当。

CRE:实测MSVC中虚基类的成员会被放在对象的最后。



总结

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

至于没有存在那四种情况而又没有声明任何构造函数的类,我们说它们拥有的是implicit trivial default constructors,它们实际上不会被合成出来。

在合成的默认构造函数中,只有基类subobject和成员类对象会被初始化。所有其他的非静态数据成员(如整数、指针、数组等)都不会被初始化。这些初始化操作对于程序而言或许有需要,但对编译器则非必要。

C++两个常见的误解:

  1. X ❌任何class如果没有定义默认构造函数,就会被合成一个出来。❌
  2. X ❌编译器合成出来的默认构造函数会显式地设定class内每一个数据成员的默认值。❌





复制构造函数的构造操作

有三种情况,会以一个object的内容作为另一个类对象的初值。

  1. 新对象的初始化。
  2. 参数的值传递。
  3. 从函数返回对象。
// *** 使用另一个对象初始化 ***   
ClassName obj2(obj1);
// *** 赋值初始化 *** (其实调用的是复制构造函数,而不是operator=操作符)    
ClassName obj2 = obj1; // 调用复制构造函数  
// *** 值传递 ***   
void func(ClassName obj); // 传递参数时调用复制构造函数
ClassName obj1;
func(obj1);
// *** 返回对象 ***   
ClassName func() {
    ClassName obj;
    return obj; // 返回对象时调用复制构造函数
}
ClassName obj1 = func();



默认的逐成员初始化(Default Memberwise Initialization)

如果class没有提供一个显式的复制构造函数。当类对象以另一个同类对象作为初值,其内部是以所谓的“default memeberwise initialization”手法完成的,也就是把每一个内建或则派生的数据成员的值,从一个对象拷贝到另一个对象身上。不过它并不会拷贝其中的成员对象,而是以递归的方式施行memeberwise initialization。

就像默认构造函数一样,C++Standard上说,如果class没有声明一个复制构造函数,就会有隐式的声明或隐式的定义出现。

C++Standard同样把复制构造函数区分为trivial和nontrivial两种。只有nontrivial的实例才会被合成于程序之中。(决定一个复制构造函数是否为trivial的标准在于class是否展现出所谓的bitwise copy semantics



Bitwise Copy Semantics(逐位复制)

一个class没有定义显式的复制构造函数,如果该class展现出”Bitwise Copy Semantics“,就不需要合成出一个默认复制构造函数。

如果一个class有一个类对象成员,这个类对象的class有复制构造函数。这种情况下,编译器必须合成出一个复制构造函数,以便调用成员类对象的复制构造函数。
类似地,如果一个class有一个基类并且基类有一个复制构造函数。那么编译器也会合成一个默认复制构造函数,来调用基类的复制构造函数。

  1. 当class内含类对象成员,并且类对象成员声明了一个复制构造函数。(不论是被显式声明还是被编译器合成)
  2. 当class继承自一个基类而后者存在一个复制构造函数时。(不论是被显式声明还是被编译器合成)
  3. 当class声明了虚函数。
  4. 当class的继承链中有虚基类。



重新设定虚函数表的指针

当编译器导入一个vptr到class之中时,该class就不再展现出bitwise semantics了,现在编译器需要合成出一个复制构造器以求将vptr适当地初始化。

合成出来的某个class的复制构造函数会显式地设定object的vptr指向该class的virutal table,而不是从右手边的类对象中将其vptr现值拷贝过来。



处理虚基类subobject

虚基类会使bitwise copy semantics失效。

有些情况下编译器必须合成一个复制构造函数,安插一些代码以设定虚基类指针/偏移的初值(或者只是简单地确定它没有被抹除),对每一个成员执行必要的memberwise初始化,以及执行其他的内存相关工作。





程序转化语义



显式的初始化操作

必要的程序转化有两个阶段:

  1. 重写每一个定义,其中的初始化操作会被剥除。
  2. class的复制构造函数调用操作会被安插进去。
X x1(x0);//复制构造函数    
X x2 = x0;//GPT:看起来像赋值,但在C++中称为复制初始化。  
X x3 = X(x0);//GPT:使用x0创建临时对象然后用来初始化x3,但会被编译器使用返回值优化(RVO)。    

转化之后的代码:

//

X x1;//初始化操作被剥离  
X x2;//初始化操作被剥离  
X x3;//初始化操作被剥离  

//编译器安插X复制构造函数的调用操作    
x1.X::X(x0);
x2.X::X(x0);
x3.X::X(x0);



参数和返回值的初始化

C++Standard说,把一个类对象当作参数传给一个函数(或者作为一个函数的返回值),相当于以下形式的初始化操作:X xx = arg;。其中xx代表形参而arg代表实参。

对象作为返回值时,如何从局部对象中拷贝出来?cfront的做法是引入一个object引用参数。(形如void foo(XXX& __result);



返回值优化

直接在返回的调用上下文中构造返回的对象。

程序员可以通过编写符合一定模式的代码帮助编译器进行优化。例如使用临时对象。

MyClass createObject() {
    return MyClass();  // RVO: MyClass对象直接构造在调用者的内存中
}
MyClass obj = createObject();  // 不会调用拷贝或移动构造函数

使用result参数取代具名的返回值,称为具名返回值优化。

X foo()
{
    X xx;
    //...
    return xx;
}
//xx用__result取代  
void foo(X& __result)
{
    __result.XX::X();
    //...
    return;
}

NRV优化如今被视为标准C++编译器的一个义不容辞的优化操作,虽然其需求其实超越了正式标准。
虽然NRV优化提供了重要的效率改善,但它还是保守批评。一个原因是不清楚编译器的实现程度,另一个原因是一旦函数变得复杂,优化就变得难以施行。第三个原因是有时候不希望程序被优化,程序比较快,但却是错误的。



什么时候需要复制构造函数

如果一个类,它既没有任何成员类或者基类带有复制构造函数,也没有任何的虚基类或者虚函数。那么它的默认复制构造函数就被视为”trivial”。这个类的bitwise copy快速且安全。

Point::Point(const Point& rhs)
{
    _x = rhs._x;
    _y = rhs._y;
    _z = rhs._z;
}
//使用memcpy更高效  

Point::Point(const Point& rhs)
{
    memcpy(this, &rhs, sizeof(Point));
}





成员们的初始化序列(Member Initalization List)

当你写下一个构造函数时,就有机会设定类成员的初值。要不是经由成员初始化序列,就是在构造函数体之内。

在下列4种情况下,为了让你的程序顺利编译,你必须使用成员初始化序列:

  1. 当初始化一个reference成员时。
  2. 当初始化一个const成员时。
  3. 当调用一个基类的构造函数,而它拥有一组参数时。
  4. 当调用一个成员类的构造函数,而它拥有一组参数时。

CRE:放在函数体中初始化也能编译通过,但是效率不高。(比如一个对象成员如果在函数体中初始化可能产生临时对象)

注意,list中的项目顺序是由class中的members声明顺序决定的,不是由initialization list中的排列顺序决定的。顺序不对可能导致bug。

编译器会对initialization list 一一处理并可能重新排序,以反映出members的声明顺序。它会安插一些代码到构造函数体内,并置于任何显式的用户代码之前。

(END)