构造函数语义学——Default Constructor的构造操作

前言

 
《C++ Annotated Reference Manual》(ARM)中曾提及:“default constructor…在需要的时候被编译器产生。”关键在于何时是被需要的时刻?

1
2
3
4
5
6
7
8
class Foo{public: int val;Foo *pnext;};
void foo_bar(){
//需求bar's member均初始化为0
Foo bar;
if(bar.val || bar.pnext){
...//do sth
}
}

在上述代码中,正确的程序语意要求bar的member被初始化为0,那么它符合ARM所说的“在需要的时刻”吗?答案是否定的。ARM所说的需要是编译器需要,而非程序语意需要。

C++ standard已经修改了ARM中的说法,但是实际相差不大。C++ standard约定:如果没有任何user—declared constructor,那么编译器会暗中(implictitly)自动生成一个default constructor(事实上标准会对此默认构造函数的修饰词是trival…)。

一个non-trivial default constructor才是编译器所需要的,下面的四个小节将分别讨论nontrival default constructor的四种情况。


“带有Default Constructor”的Member Class Object

 
如果一个class没有任何constructor,但它内含一个member object且后者带有一个default constructor,那么该class的implict default constructor就是所谓的“non-trival”,编译器需要为此class合成出一个default constrcutor。不过该合成操作仅仅发生于constructor真正需要被调用时。

那么在C++各个不同的编译模块中,编译器如何避免生成多个default constructor?解决方法是把合成的default constructor、copy constructor、destructor、assignment copy constuctor均以inline形式完成。一个inline函数具备静态链接(static linkage),无法被档案以外者观察到。如果函数过于复杂,那则合成出explicit non-inline static实体。

实例

在以下程序片段中,编译器会为class Bar合成一个default constructor:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Foo{
public:
Foo();
Foo(int);
};
class Bar{
public:
Foo foo;
char* str;
};
void foo_bar(){
Bar bar;//此处bar被初始化,原因在于foo具备默认初始化
}

被合成的Bar default constructor内含必要的代码,能够调用class Foo的default constructor来处理member object Bar::foo,但它并不产生任何代码来初始化Bar::str。被合成的default constructor看起来可能像这样:
1
2
3
inline Bar::Bar(){
foo.Foo::Foo();
}

为了保证上述程序片段正常运行,str也需要被初始化。我们假定程序员经由下面的default constructor提供了str的初始化操作:
1
Bar::Bar() {str=nullptr;}

现在程序的要求已经得到了满足,但编译器还需要初始化memeber object foo。由于default constructor已经被明确定义,所以编译器不会生成第二个。那么编译器会如何执行?
编译器的执行策略是:“如果class A中含有一个或一个以上的member class objects,那么class A的每一个constructor必须调用每一个member classes的default constuctor”。编译器会在已经编写的constructors中插入必要的代码,保证在user_code执行之前,先调用必要的default constructor。仍以Bar为例,其具体的构造函数大致如下所示:
1
2
3
4
Bar::Bar(){
foo.Foo::Foo();
str=nullptr;
}

如果具备多个class member objects,那么它们的初始化顺序何如?编译器的解决方案是:以“member objects在class中的声明次序”来调用各个constructors。


“带有Default Constructor的Base Class”

 
如果一个没有任何constructors的class派生自一个“带有default constructor”的base class,那么这个derived class的deafualt constructor会被视为non-trivial,并且根据需要被合成出来。它会根据base class的声明顺序依次调用base class的default constructor。

但如果设计者提供了多个constructors,但没有default constructor呢?编译器会扩张现有的每一个constructors,将“用以调用所有必要之default constructor”的程序代码加入其中。编译器不会去合成一个新的constructor,因此用户已经编写了其他constructors。如果同时亦存在着“带有default constructors”的member class objects,那些default constructor也会被调用——在所有base class constructor都被调用之后。

(我的理解是,如果base class具备多个构造函数,但不具备无参构造函数,那么编译器会在每一个有参构造函数中增加代码使其具备无参构造性,也就是可以理解为缺什么就再调用什么的default constructor)


“带有一个Virtual Function”的class

 
另有两种需要合成default constructor的情况:

  1. class声明(或继承)一个virtual function。
  2. class所在的继承体系中存在一个或多个virtual base classes。

上述两种情况由于缺乏user声明的constructors,编译器会详细记录合成一个default constructor的必要信息。

实例

以如下程序段为例:
image_1ccfn2t8p1gst10ks1v6o7pb1cjo9.png-11.4kB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Widget{
public:
virtual void flip() = 0;
...
}
void flip(const Widget& widget){
widget.flip();
}
void foo(){
Bell b;
Whistle w;
flip(b);
flip(w);
}

以下两个扩张操作会在编译期完成:

  1. vtbl被编译器产生,内放class的virtual functions地址。
  2. 在每一个class object中,编译器会放置一个vptr指向vtbl。

此外,widget.flip()的virtual invocation会被改写:

1
*widget.vptr[1](&widget);//&widget代表着this指针

为了保证该机制生效,编译器必须为每一个Widget(包括其派生类)object的vptr设定初值。对于class所定义的每一个constructor,编译器都会扩张它们,在其中放入vptr的初始化。对于内部未声明constructor的classes,编译器会为它们合成一个default constructor,以便正确地初始化每一个class object的vptr。


“带有一个Virtual Base class”的class

 
尽管virtual base class的实现在不同编译器间存在较大差异,但共同点在于必须使每一个virtual base class在其每一个derived class object中的位置在执行期准备妥当。

实例

以如下代码及继承体系为例:
image_1ccfo2g9e1n84rf81pjt17301c58m.png-7.9kB

1
2
3
4
5
6
7
8
9
10
11
class X{public: int i;};
class A:public virtual X {public:int j;};
class B:public virtual X {public:double d;};
class C:public A,public B {public:int k;};

void foo(const A* pa) {pa->i=1000;}//无法在编译期了解pa->X::i的位置

main(){
foo(new A);
foo(new C);
}

编译器无法固定住foo()之中“经由pa而存取的X::i”的实际偏移位置,因为pa的动态类型是不确定的。编译器的操作是改变执行存取操作的那些代码,从而令X::i可以延缓到运行期执行。

cfront实例

以cfront编译器为例,其做法是在derived class object的每一个virtual base classes中安插一个指针。所有经由reference或pointer来存取一个virtual base class的操作都由该指针完成,如此一来,foo被改写为:

1
2
3
void foo(const A* pa){
pa->_vbcx->i = 1000;
}

_vbcx是编译器所产生的指针,指向virtual base class X。
_vbcx是在class object建构期间被完成得。对于class所定义的每一个constructor,编译器会在其中扩展代码,保证允许在执行期确定virtual base class的地址。如果class没有声明任何constructors,编译器就会为其合成一个。


总结

 
有四种情况会导致编译器为没有声明construtor的classes合成default constructor,C++ standard将这些被合成物称为implicit non-trivial default constructors。被合成物满足的编译器的需要而非程序员的需要。在这四种情况之外且没有声明任何constructor的classes,拥有的是implicit trivial default constructor,实际上它们并不会被合成出来。

在合成的default constructor中,只有base subobject与member class objects会被初始化,所有其他的non-static data member均不会初始化,因为这些操作对编译器而言并非必须,因此需要程序员手动完成。

C++新手一般会有两个误解:

  1. 任何class如果没有声明default constructor就会被合成出一个。
  2. 编译器合成出来的default constructor会初始化所有class member。

以上两条都是错的。