构造函数语义学(The Semantics of Constructors)

Default Constructor的构造操作

对于class X,如果没有任何user-declared constructor,那么会有一个default constructor被隐式(implicitly)声明出来...一个被隐式声明出来的default constructor将是一个trivial(浅薄而无能,没啥用的)constructor...

一个nontrivial default constructor在ARM(注释参考手册)的术语中就是编译器需要的那种,必要的话由编译器合成出来。下面4小节分别讨论nontrivial default constructor的4种情况

“带有Default Constructor”的member class object

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

举例如下:编译器会为class Bar合成一个default constructor:

class Foo{ public: Foo(), Foo(int) ...};
class Bar{ public: Foo foo; char *str; }; void foo_bar(){
Bar bar; //注意Bar::foo必须在此初始化
if(str) { } ...
}

被合成的Bar default constructor内含必要的代码,能够调用class Foo的default constructor来处理member Bar::foo,但它并不产生任何码来初始化Bar::str。将Bar::foo初始化时编译器的责任,将Bar::str初始化则是程序员的责任。

如果有多个class member objects都要求constructor初始化操作,将如何? C++语言要求以“member objects在class中的声明顺序”来调用各个constructors

“带有Default constructor”的base class

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

“带有一个Virtual Funtion”的class

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

不管哪一种情况,由于缺乏由user声明的constructors,编译器会详细记录合成一个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(在cfront中被称为vtbl)会被编译期产生出来,内放class的virtual functions地址
  • 在每一个class object中,一个额外的pointer member(也就是vptr)会被编译期合成出来,内含相关之class vtbl的地址

此外,widget.flip()的虚拟引发操作(virtual invocation)会被重新改写,已使用widget的vptr和vtabl中的flip()条目

//widget.flip()的虚拟引发操作的转变
(*widget.vptr[1])(&widget)

其中:

  • 1 表示flip()在virtual table中的固定索引
  • &widget代表要交给“被调用的某个flip()函数实体”的this指针

“带有一个virtual base class”的class

Virtual base class的实现法在不同的编译器之间有极大的差异。然而,每一种实现法的共同点在于必须使virtual base在其每一个derived class object中的位置,能够于执行期准备妥当。

Copy Constructor的构造操作

有三种情况,会以一个object的内容作为另一个class object的初值,最明显的一种情况是当对一个object做明确的初始化操作,像这样:

class X{ ... };
X x; //明确以一个object的内容作为另一个class object的初值
X xx = x;

另外两种情况是当object被当做参数交个某个函数时,例如:

extern void foo(X x);

void bar(){
X xx; //以xx作为foo()第一个参数的初值(不明显的初始化操作)
foo(xx);
}

以及当函数传回一个class object时,例如:

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

假设class设计者明确定义了一个copy constructor(这是一个constructor,有一个参数的类型是其class type),像下面这样:

//user-defined copy constructor实例
//可以是多参数形式,其第二个参数及后继参数以一个默认值供应之
X::X(const X& x);
Y::Y(const Y& y, int = 0);

那么在大部分情况下,当一个class object以另一个同类实体作为初值时,上述的constructor会被调用,这可能会导致一个暂时性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。例如:

class String{
public:
//... 没有explicit copy constructor
private:
char *str;
int len;
};

一个String object的default memberwise initialization发生在这种情况之下:

String noun("book");
String verb = noun;

其完成方式就好像个别设定每一个members一样:

//语义相等
verb.str = noun.str;
verb.len = noun.len;

一个class object可用两种方式复制得到,一种是被初始化,另一种是被指定。从概念上看,这两种操作分别是以copy constructor和copy assignment operator完成的。

Bitwise Copy Semantics(位逐次拷贝)

什么时候一个class不展现出“bitwise copy semantics”呢?有4种情况:

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

前两种情况中,编译器必须将member或base class的"copy constructors调用操作"安插到被合成的copy constructor中。

重新设定Virtual Table的指针

回忆编译期间的两个程序扩张操作(只要有一个class声明了一个或多个virtual functions就会如此):

  • 增加一个virtual function table(vtbl),内含每一个有作用的virtual function的地址
  • 将一个指向virtual function table的指针(vptr),安插在每一个class object内

当编译器导入一个vptr到class之中时,该class就不再展现bitwise semantics了。现在编译器需要合成出一个copy constructor,以求将vptr适当地初始化。下面是个例子

class ZooAnimal{
public:
ZooAnimal();
virtual ~ZooAnimal();
virtual void animate();
virtual void draw();
//...
private:
//ZooAnimal的animate()和draw()所需要的数据
}; class Bear : public ZooAnimal{
public:
Bear();
void animate(); //虽未写明是virtual,但其实是virtual
void draw(); //同上
virtual void dance();
//...
private:
//Bear的animate()和draw()和dance()所需要的数据
};

ZooAnimal class object以另一个ZooAnimal class object作为初值,或Bear class object以另一个Bear class object作为初值,都可以直接靠"bitwise copy semantics"完成。举个例子

Bear yogi;
Bear winnie = yogi;

yogi会被default Bear constructor初始化,而在constructor中,yogi的vptr被设定指向Bear class的virtual table(靠编译器安插的码完成),因此,把yogi的vptr值拷贝给winnie的vptr是安全的。

深入探索C++对象模型(二)-LMLPHP

当一个base class object以其derived class的object内容做初始化操作时,其vptr复制操作也必须保证安全,例如:

ZooAnimal franny = yogi;   //这会发生切割(sliced)行为

franny的vptr不可以被设定指向Bear class的virtual table(但如果yogi的vptr被直接“bitwise copy”的话,就会导致此结果),否则当下面程序片段中的draw()被调用而franny被传进去时,就会“炸毁”(blow up):

void draw(const ZooAnimal& zoey) { zoey.draw(); }
void foo(){
//franny的vptr指向ZooAnimal的virtual table,
//而非Bear的virtual table(彼由yogi的vptr指出)
ZooAnimal franny = yogi; draw(yogi); //调用Bear::draw()
draw(franny); //调用ZooAnimal::dram()
}

深入探索C++对象模型(二)-LMLPHP

也就是说,合成出来的ZooAnimal copy constructor会明确设定object的vptr指向ZooAnimal class的virtual table, 而不是直接从右手边的class object中将其vptr现值拷贝过来。

处理virtual base class subobject

virtual base class的存在需要特别处理。一个class object如果以另一个object作为初值,而后者有一个virtual base class subobject,那么也会使“bitwise copy semantics”失效

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

class Raccoon : public virtual ZooAnimal{
public:
Raccoon() { /*设定private data初值*/ }
Raccoon(int val) { /*设定private data初值*/}
//...
private:
//所有必要的数据
};

编译器所产生的代码(用以调用ZooAnimal的default constructor、将Raccoon的vptr初始化,并定位出Raccoon中的ZooAnimal subobject)被安插在两个Raccoon constructors之内,成为其先头部队

在"memberwise 初始化"呢? 一个virtual base class的存在会使bitwise copy semantics无效,注意,这个问题并不发生在“一个class object以另一个同类的object作为初值”之时,而是发生在“一个class object以其derived classes的某个object作为初值”之时。举例如下:

class RedPanda : public Raccoon{
public:
RedPanda() { /*设定private data初值*/ }
RedPanda(int val){ /*设定private data初值*/ }
//...
private:
//所有必要的数据
};

强调,如果以一个Raccoon object作为另一个Raccoon object的初值,那么bitwise copy就绰绰有余了。

//简单的bitwise copy就足够了
Raccoon rocky;
Raccoon little_critter = rocky;

然而如果企图以一个RedPanda object作为little_critter的初值,编译器必须判断“后续当程序员企图存取其ZooAnimal subobject时是否能够正确地执行”

//简单的bitwise copy还不够
//编译器必须明确将little_critter的
//virtual base class pointer/ooset初始化
RedPanda little_red;
Raccoon little_critter = little_red;

在这种情况下,为了完成正确的little_critter初值设定,编译器必须合成一个copy constructor,安插一些码以设定virtual base class pointer/offset的初值(或只是简单地确定它没有被抹消),对每一个members执行毕业得memberwise初始化操作,以及执行其它的内存相关工作。

深入探索C++对象模型(二)-LMLPHP

程序转化语意学(Program Transformation Semantics)

显式的初始化操作

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

  • 重写每一个定义,其中的初始化操作会被剥除
  • class的copy constructor调用操作会被安插进去

参数的初始化

C++ Standard说,把一个class object当做参数传给一个函数(或是作为一个函数的返回值),相当于以下形式的初始化操作:

X xx = arg;

其中xx代表形式参数(或返回值)而arg代表真正的参数值,因此,若已知如下函数:

void foo(X xo);

下面的调用方式:

X xx;
//...
foo(xx);

将会要求局部实体(local instance) xo以membeerwise的方式将xx当作初值。

在编译器实现技术上,有一种策略是导入所谓的临时性object,并调用copy constructor将它初始化,然后将此临时性object交给函数。例如,前一段的代码转换如下:

//C++伪码
//编译器产生出来的临时对象
X _temp0; //编译器对copy constructor的调用
_temp0.X::X(xx);
//重新改写函数调用操作,以便使用上述的暂时对象
foo(_temp0);

然而这样的转换只做了一半功夫而已,残留问题如下:问题出在foo()的声明。暂时性object先以class X的copy constructor正确设定了初值,然后再以bitwise防守拷贝到xo这个局部实体中(所以,不能按照以往的声明)。因此,foo()的声明因而也必须被转化,形式参数必须从原先一个class X object改变为一个class X reference。如下:

void foo(X& xo);

其中class X声明了一个destructor,它会在foo()函数完成之后被调用,对付那个暂时性的object。

另外一种实现方法是以“拷贝建构”(copy construct)的方式把实际参数直接建构在其应该的位置上,此位置视函数活动范围的不同,记录于程序堆栈中。在函数返回之前,局部对象(local object)的destructor(如果有定义的话)会被执行。

返回值得初始化

已知下面这个函数定义:

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

bar()的返回值如何从局部对象xx中拷贝过来? Stroustrup在cfront中的解决办法是一个双阶段的转化:

  1. 首先加上一个额外参数,其类型是class object的一个reference,这个参数将被用来放置被“拷贝建构”而得的返回值
  2. 在return指令之前安插一个copy constructor调用操作,以便将欲传回之object的内容当做上述新增参数的初值。

真正的返回值是什么? 最后一个转换操作会重新改写函数,使它不传回任何值。bar()转换如下:

//函数转换,以反映copy constructor的应用
void bar(X& _result){
X xx; //编译器所产生的default constructor调用操作
xx.X::X(); //...处理 xx //编译器所产生的copy constructor调用操作
_result.X::X(xx); return;
}

在编译器层面做优化

在一个如bar()这样的函数,所有的return指令传回相同的具名数值(name value,即是指函数中的xx),因此编译器有可能自己做优化,方法是以result参数取代name return val。例如原bar()函数,可能被转换为:

void bar(X& _result){
//default constructor被调用
_result.X::X(); //...直接处理_result; return;
}

这样的编译器优化操作,有时被称为Named Return Value(NRV)优化。 NRV优化如今被视为是标准C++编译器的一个义不容辞的优化操作——虽然其需求其实超越了正式标准之外。

虽然NRV优化提供了重要的效率改善,它还是饱受批评。其中一个原因是,优化由编译器默认完成,而它是否真的被完成,并不十分清楚。第二个原因是,一旦函数变得比较复杂,优化也就变得比较难以施行。

下面例子,三个初始化操作在语义上相等:

X xx0(1024);
X xx1 = X(1024);
X xx2 = (X)1024;

但是在第二行和第三行中,语法明显提供了两个步骤的初始化操作:

  1. 将一个暂时性的object设以初值1024
  2. 将暂时性的object以拷贝建构的方式作为explicit object的初值

换句话说,xx0是被单一的constructor操作设定初值:

xx0.X::X(1024);

而xx1或xx2却调用两个constructor,产生一个暂时性object,并针对该暂时性object调用class X的destructor

X _temp0;
_temp0.X::X(1024);
xx1.X::X(_temp0);
_temp0.X::~X();

一般而言,面对“以一个class object作为另一个class object的初值”的情形,语言允许编译器有大量的自由发挥空间。其利益当然是导致机器码产生时有明显的效率提升。缺点则是你不能安全地规划你的copy constructor的副作用,必须视其执行而定。

Copy Constructor:要还是不要?

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

成员们的初始化队伍(Memeber Initialization List)

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

  • 当初始化一个reference member时
  • 当初始化一个const member时
  • 当调用一个base class的constructor,而它拥有一组参数时
  • 当调用一个member class的constructor,而它拥有一组参数时

下列情况下,程序可以被正确编译并执行,但是效率不彰,例如:

class Word{
String _name;
int _cnt;
public:
//没有错误,只不过太天真
Work(){
_name = 0;
_cnt = 0;
}
};

在这里,Word constructor会先产生一个暂时性的String object,然后将它初始化,再以一个assignment运算符将暂时性object指定给_name,然后再摧毁那个暂时性对象。以下是constructor可能的内部扩张结果:

Word::Word( /*this pointer goes here*/ ){
//调用String的default constructor
_name.String::String(); //产生暂时性对象
String temp = String(0); //"memberwise"地拷贝_name
_name.String::operator=(temp); //摧毁暂时性对象
temp.String::~String(); _cnt = 0;
}

对程序代码反复审查并修正之,得到一个明显更有效率的实现方法:

//较佳的方式
Word::Word : _name(0){
_cnt = 0;
}

它会被扩张成如下样子:

Word::Word( /*this pointer goes here*/ ){
//调用String(int) constructor
_name.String::String(0);
_cnt = 0;
}

member initialization list中到底会发生什么事情?编译器会一一操作initialization list,以适当顺序在constructor之内安插初始化操作,并且在任何explicit user code之前。

initialization list中的项目顺序是由class中的members声明顺序决定的,不是由initialization list中的排列顺序决定的。

05-21 10:10