C++语言编译过程中,编译器会背着程序员做了很多工作,其中默认构造函数就是其中一件工作。编译器对代码的干涉动作比较可能发生在“Member initialization”或者“named return value optimization”(NRV)身上,这些干涉动作会带来“程序形式”和“程序效率”上的冲击。
1.名词解释
- implicit:暗中的,隐式的(通常指并非在源代码中出现的)
- explicit:显示的(通常意指成员源码中所出现的)
- trivial:没有用的
- nontrivial:有用的
- memberwise:对每一个member施加以...
- bitwise:对每一个bit施加以...
2. Default Constructor的构造操作
只有在编译器需要的时候才会合成一个default constructor,且被合成的default constructor只执行编译器所需的行动。C++ Standard中:
“对于class X,如果没有任何user-declared constructor,那么会有一个default constructor被暗中(implicitly)声明出来......一个被暗中声明出来的default constructor将是一个trivial constructor.......”
nontrivial default constructor生成的四种情况:
(1)带有Default Constructor的Member Class Object
如果一个class没有任何constructor,但它内含一个member object,而后者有default constructor,那么这个class的implicit default constructor 就是“nontrivial”,编译器需要为该class 合成一个default constructor。不过这个合成操作只有在constructor真正需要被调用的时候才会发生。
被合成的default constructor内含必要的代码,能够调用member object的default constructor来处理构造操作,除此之外,不会合成其他的代码。
举个例子,在下面的程序片段中,编译器为class Bar合成了一个default constructor。
class Foo { public: Foo(), Foo(int) ...};
class Bar { public: Foo foo; char* str};
void foo_bar()
{
Bar bar; // 这里需要调用Bar的构造函数,编译器会合成一个
// default constructor,如果没有任何地方调用
// Bar的构造函数,编译器不会为其合成,即使Bar
// 没有默认的构造函数
if (bar.str) {}...
}
被合成的Bar default constructor内含必要的代码,能够调用class Foo的default constructor来处理member object Bar::far, 但是它并不会产生代码来初始化Bar::str。被合成的default constructor的伪代码如下:
inline Bar::Bar()
{
foo.Foo::Foo();
}
再一次请注意,合成的代码只是满足编译器的需要,而不是满足程序员的需要,因此,对str的初始化是程序员的责任。为了让这段代码顺利执行,字符指针str也必须被初始化,假设程序员提供了下面的default constructor,对str进行了初始化。
Bar::Bar() { str = 0; }
现在程序员的要求满足了,但是编译器还需要初始化member object foo,由于default constructor已经显示定义出来,编译器没法合成第二个。这种情况编译器会如何处理?
如果class有default constructor。编译器就不会合成default constructor,而是扩充已有的default constructor。如果class A内含一个或一个以上的member class objects,那么class A的每一个constructor必须调用每一个member classes的default constructor。编译器会扩张已存在的constructors,在其中安插一些代码,使得user code在被执行之前,先调用必要的default constructor。如果有多个class member objects都要求constructor初始化操作,C++语言将要求以member objects在class中的声明次序来调用每个constructor。这一点由编译器完成。请看下面的例子,假设有三个class定义如下:
class Dopey { public: Dopey();...};
class Sneezy { pyblic: Sneezy(int); Sneezy();...}
class Bashful {public: Bashful();...}
以及一个class Snow_White:
class Snow_White
{
public:
Dopey dopey;
Sneezy sneezy;
Bashful bashful;
private:
int mumble;
};
如果Snow_White 没有定义default constructor,就会有一个nontrivial constructor被合成出来,依序调用Dopey,Sneezy,Bashful的default constructor,然而,如果Snow_White定义了下面这样的default constructor:
Snow_White::Snow_White : sneezy(1024)
{
mumble = 2048;
}
它会扩张为:
// 编译器扩张后的default constructor
// C++ 伪代码
Snow_White::Snow_White() : sneezy(1024)
{
// 插入 member class object
// 调用其constructor
dopey.Dopey::Dopey();
sneezy.Sneezy::Sneezy(1024);
bashful.Bashful::Bashful();
// explicit user code
mumble = 2048;
}
(2)带有Default Constructor的Base Class
类似的道理,如果一个没有任何constructor的class派生自一个“带有default constructor”的base constructor,那么这个derived class的default constructor会被视为nontrivial,并因此需要被合成出来。它将会调用上一层的base class的default constructor(依据它们声明的顺序)。对一个后继派生的class而言,这个合成的constructor和一个“被显式提供的default constructor”没有什么差异。
如果设计者提供多个constructor,但其中都没有default constructor,那么编译器会扩张现有的每一个constructor,将“用以调用所有必要之default constructor”的程序代码加进去,它不会合成一个新的default constructor,因为其他“有user所提供的constructor”存在的缘故。如果同时存在着“带有default constructor”的member class objects,那些default constructor也会在base class constructor被调用之后,依次被调用。
(3)带有一个Visual Function的Class
另有两种情况,也需要合成出Default constructor:
-
class声明(或继承)一个visual function。
- class派生自一个继承串链,其中有一个或更多的visual base class。
不管是哪一个情况,由于缺乏由user声明的constructor,编译器会详细记录合成一个default constructor的必要信息,以下面代码为例:
class Widget { public: virtual void flip() = 0;}
void flip(const Widget& widget) { widget.flip();}
// 假设Bell 和 Whistle 都派生自Widget
void foo()
{
Bell b;
Whistle w;
flip(b);
flip(w);
}
下面的两个扩张操作会在编译期间发生:
- 一个virtual function table(vtbl)会被编译出来,内放class的virtual functions地址
- 在每一个class objects中,一个额外的pointer member(也就是vptr)会被编译器合成出来,内含相关的class vtbl的地址。
此外,widget的flip的虚拟调用,会被重写改写,以使用widget的vptr和vtbl中的flip()条目:
// widget.flip的虚拟调用操作的转变
(*widget.vptr[1])(&widget)
- 1 表示flip在virtual table中的固定索引
- &widget代表要交给“被调用的某个flip函数实例”的this指针
为了让此机制发挥功效,编译器必须为每一个Widget(或其派生类) object的vptr设置初值,放在恰当的virtual table 地址,对于class所定义的每一个constructor,编译器会安插一些代码做这样的事情。
(4)带有一个visual base class的class
对于class 所定义的每一个constructor,编译器会安插那些“允许每一个virtual base class的执行期存取操作”的代码,如果class 没有声明任何constructor,编译器必须为它合成一个default constructor。
小结:
C++有两个误解:
1)任何class如果没有定义Default constructor,就会被合成出一个来;
2)编译器合成出来的Default constructor会明确设定“class 内每一个data member的默认值”。
上面的两个都是错误的。
然而,事实上,C++合成默认构造函数只是在编译器需要的情况下才会合成,并且只有四种情况,编译器会为未声明constructor的classes合成一个default constructor,这些合成的为成为 implicit nontrivial default constructor。至于没有存在那4种情况而又没有声明任何constructor的classes,我们说它们拥有implicit trivial default constructor,它们实际不会被合成出来。
3.Copy Constructor的构造操作
有三种情况,会以一个object的内容作为另一个class object的初值。
- 对一个object做明确的初始化操作。
- 当object被当做参数交给某个函数。
- 当函数返回一个class object。
(1)Default Memberwise Initialization
当class object以“相同class的另一个object”作为初值时,其内部是以所谓的default member initialization手法完成的,也就是把每一个内建的或派生的data member的值,从某个object拷贝一份到另一个object身上。不过它并不会拷贝其中的member class object,而是以递归的方式实施memberwise initialization。 决定一个copy Constructor是否为trivial的标准在于class是否展现出所谓的“bitwise copy semantics”。bitwise copy semantics(位逐次拷贝)意思就是:使用bitwise copy semantics时就不会使用default Constructor或copy Constructor。
(2)bitwise copy semantics
有四种情况不需要bitwise copy semantics:
- 当class 内含一个member object而后者的class声明有一个copy Constructor时。
- 当class继承自一个base class而后者存在有一个copy Constructor时。
- 当class声明了一个或多个virtual functions时。
- 当class派生自一个继承串链,其中有一个或多个virtual base classes时。
前两种情况中,编译器必须将member或base class的“copy Constructor调用操作”安插到被合成的“copy constructor"中。后两种情况中,存在virtual functions和virtual base classes,如果使用位逐次拷贝,其vptr就会出错。
后两种情况会出现重新设定virtual table的指针,只要有一个class声明了一个或多个virtual functions就会如此。
(3)扩张操作
- 增加一个virtual function table(vtbl),内含每一个有作用的virtual function的地址。
- 将一个指向virtual function table的指针(vptr),安插在每一个class object内。
当一个编译器导入一个vptr到class之中时,该class就不再展现bitwise semantics了,故编译器需要合成一个copy constructor,以求将vptr适当地初始化。
4. 程序转化语意学
(1)明确的初始化操作
必要的程序转化包括两个阶段:(1)重写每一个定义,其中的初始化操作会被剥夺;(2)class的copy constructor调用操作会被安插进去。
(2)参数的初始化
其中一种策略:导入所谓的暂时性object,并调用copy constructor将它初始化,然后将该暂时性object交给函数,函数调用完成后,临时对象将会被析构。还有一种策略”拷贝构建“,将实际参数直接建造在其应该的位置上。
(3)返回值的初始化
named return value(NRV)优化机制。有时候看似不需要拷贝构造函数,但是为了NRV效率问题,也可以提供一个默认拷贝构造函数。
小结:
拷贝构造函数的使用,迫使编译器对程序代码做部分优化,尤其当一个函数以传值(by value)的方式传回一个class object,而该class有一个copy constructor时,这将导致深奥的程序转化——不论在函数的定义或使用上。此外编译器也将copy constructor的调用操作优化,以一个额外的第一参数取代NRV。
5.成员们的初始化队伍(Member Initialization List)
当写一个constructor时,有机会设定class members的初值。要不是经由member Initialization list就是在构造函数内部进行数据成员的初始化。
下面四种情况必须使用成员初始化列表:
- 当初始化一个reference member时;
- 当初始化一个const member时;
- 当调用一个base class的constructor,而它拥有一组参数时;
- 当调用一个member class的constructor,而它拥有一组参数时。
编译器会对Initialization list一一处理并可能重新排序,以反映出member的声明次序。它会安插一些代码到constructor体内,并置于任何explicit user code之前。
总结:
主要介绍了default constructor、copy constructor还有何种情况使用到copy constructor以及成员初始化列表。其中default constructor只是在四种情况下才会被编译器构造出来,copy constructor和位逐次拷贝,在一些含有copy constructor的类中或者有virtual中,编译器需要合成copy constructor而不能使用位逐次拷贝,成员初始化列表在四种情况下必须使用。