构造函数扩充
当我们写下这样的表达式:1
T object;
显然,T类型的constructor会被调用,但我们不太了解constructor的调用还伴随着什么。Constructor可能带有大量的隐藏代码,因为编译器会扩充每一个constructor,扩充程序视当前class T的继承体系而定,一般而言编译器所做的扩充操作大致如下:
- 记录在member initialization list中的data members初始化操作会被放进constructor函数本身,并以member的声明顺序为执行次序。
- 如果有member并未出现在member initialization list中且它有一个default constructor,则该default constructor被调用。
- 如果class object有vptr,则vptr会被设定初值,指向适当的vtbl(这发生在所有member构造或赋值之前)。
- 所有上一层的base class constructors必须被调用,其调用顺序以继承规格中的声明顺序为准,而非依赖member initialization list:
如果base class存在于member initialization list之中,那么任何明确指定的参数都应当被传入
如果base class并未存在于member initialization list之中,但其存在一个default constructor,则调用之
如果base class是多重继承下的后续base class,则this指针必须要发生调整 - 所有virtual base class constructors必须被调用,从左到右,从深到浅
如果class位于member initialization list中,则任何明确指定的参数都应当被传入,如果不在list中,则应当调用其default constructor
此外,class中的每一个virtual base class subobject的offset必须在执行期可被存取
如果class obejct是最底层的class,其constructors可能会被调用,某些用以支持该行为的机制应当被放入
问题实例
我们再一次以Point举例,本次我们为其加入一个copy constructor、一个copy assignment operator、一个virtual destructor如下:1
2
3
4
5
6
7
8
9
10class Point{
public:
Point(float x=0.0,y=0.0);
Point(const Point&);
Point& operator=(const Point&);
virtual ~Point();
virtual float z() {return 0.0;}
protected:
float _x,_y;
};
现有class Line,每一个Line可以由两个Point构造:1
2
3
4
5
6
7class Line{
Point _begin,_end;
public:
Line(float=0.0,float=0.0,float=0.0,float=0.0);
Line(const Point&,const Point&);
draw();
};
每一个explicit constructor都会被扩充以调用其两个member class objects的constructors。如果我们定义constructor如下:1
2Line::Line(const Point &begin,const Point &end):
_end(end),_begin(begin) {}//注意与声明顺序不一致
该constructor会被扩充为:1
2
3
4
5
6
7//C++伪代码
Line*
Line::Line(Line *this,const Point &begin,const Point &end){
this->_begin.Point::Point(begin);
this->_end.Point::Point(end);
return this;
}
由于Point声明了一个copy constructor、一个copy assignment operator,以及一个destructor,所以Line class的implicit copy constructor、copy operator与destructor均不为trivial。
当我们写下:1
Line a;
implicit destructor将会被合成,在implicit destructor中,它的member class objects的destructors会被调用(以其构造的相反顺序):1
2
3
4
5
6//C++伪代码
inline void
Line::~Line(Line *this){
this->_end.Point::~Point();
this->_begin.Point::~Point();
}
这里需要注意的是,尽管Point的destructor具备virtual属性,但由于在本次实例中不具备任何多态性,因此在inline函数中其会被resolved statically。
类似地,当写下Line b = a
时,copy constructor会被合成;写下a = b
时,copy assignment operator会被合成。此处还有一个知识点在于不可忘记在operator=中处理自我赋值的可能性。
虚拟继承
考虑如下虚拟继承:1
2
3
4
5
6
7
8
9
10
11
12class Point3d:public virtual Point{
public:
Point3d(float x=0.0,float y=0.0,float z=0.0)
:Point(x,y),_z(z) {}
Point3d(const Point3d& rhs)
:Point(rhs),_z(rhs._z) {}
~Point3d();
Point3d& operator=(const Point3d&);
virtual float z() {return _z;}
protected:
float _z;
};
在本例中,传统的constructor扩充现象并无作用,这是由于virtual base class具备共享性:1
2
3
4
5
6
7
8
9//不合法的扩充内容
Point3d*
Point3d::Point3d(Point3d *this,float x,float y,float z){
this->Point::Point(x,y);
this->_vptr_Point3d = _vbtl_Point3d;
this->_vptr_Point3d_Point = _vtbl_Point3d_Point;
this->_z = rhs._z;
return this;
}
上述扩充内容是错误的,其错误之处将会在下文中给予说明。
问题实例
试考虑以下三种类派生情况:1
2
3class Vertex:virtual public Point {...};
class Vertex3d:public Point3d,public Vertex {...}
class PVertex:public Vertex3d {...}
该继承体系结构如下图所示:
问题剖析
显然,Vertex的constructor必须调用Point的constructor,然而,当Point3d与Vertex同为Vertex3d的subobjects时,它们对Point constructor的调用必然不可发生,取而代之的是,作为最底层class,Vertex3d有责任初始化Point。而在更下层的继承体系中,Pvertex将负责完成Point subobject的构造。
传统的扩充策略无法区分当前是否需要初始化virtual base class,因此,它必须条件式地测试传入参数,然后再决定是否调用相关的virtual base class constructors。以下为Point3d的constructor扩充内容:1
2
3
4
5
6
7
8
9//C++伪代码
Point3d*
Point3d::Point3d(Point3d *this,bool _most_derived,float x,float y,float z){
if(_most_derived!=false) this->Point::Point(x,y);
this->_vptr_Point3d = _vbtl_Point3d;
this->_vptr_Point3d_Point = _vtbl_Point3d_Point;
this->_z = rhs._z;
return this;
}
在更深层的继承情况下,例如在Vertex3d中,当调用Point3d与Vertex的constructor时,总是会把_most_derived参数设为false,于是抑制了两个constructors中对Point constructor的调用操作:1
2
3
4
5
6
7
8
9
10Vertex3d*
Vertex3d::Vertex3d(Vertex3d *this,bool _most_derived,float x,float y,float z){
if(_most_derived!=false) this->Point::Point(x,y);
//在调用上一层constructor时总是将_most_derived设置为false
this->Point3d::Point3d(false,x,y,z);
this->Vertex::Vertex(false,x,y);
...//设定vptrs
...//出入user code
return this;
}
这种策略可以保证语义正确无误,举例而言,当我们定义:1
Point3d origin;
Point3d constructor可以正确地调用其Point virtual base class subobject。而当我们定义:1
Vertex3d cv;
时,Vertex3d constructor正确地调用Point constructor。Point3d和Vertex的constructors会做每一件该做的事情——除了调用Point。
virtual base class constructors的被调用存在明确定义:只有当一个完整的class object被定义出来(例如origin),它才会被调用,如果当前object只是某个完整object的subobject,它则不会被调用。以此作为依据我们可以撰写更加优化的编译器策略:将每一个constructor一分为二,一个针对完整的object,另一个针对subobject。它们的区别主要在于是否调用virtual base constructor以及是否设定vptr。
vptr初始化语义学
仍以上文提及的集成体系为例,当我们定义一个Pvertex object时,constructor的调用顺序为:1
2
3
4
5Point(x,y);
Point3d(x,y,z);
Vertex(x,y,z);
Vertex3d(x,y,z);
PVertex(x,y,z);
假设该继承体系中的每一个class都定义了一个virtual function size(),返回当前pointer或者reference所指向的class的大小。更进一步地,我们假定每一个constructors均内置了一个调用操作:1
2
3
4Point3d::Point3d(float x,float y,float z)
:_x(x),_y(y),_z(z){
if(spyOn) cerr << size() << endl;
}
那么当我们定义PVertex object时,前述的五个constructor将会如何?C++语言规则约定,在构造函数尚未执行完毕时,对象本身性质并不明确。这意味着当每一个Pvertex base class constructors被调用时,编译系统必须确保有适当的size()函数实体被调用。最根本的解决方案是:在执行constructor时,必须限制一组virtual function候选名单,也就是说,必须要有一个确定的vptr指向正确的vtbl。为了能够正确地调用当前对应的virtual function实体,编译系统只需要简单地控制vptr的初始化和设定操作即可。
vptr初始化应当如何被处理?改答案视vptr在constructor中“应该何时被初始化”而定。我们有三种选择:
- 在任何操作之前
- 在base class constructors调用操作之后,但在member initialization list或开发者提供的user code之前
- 在所有事情之后
正确的选择是2,我们之前也是那么做的。令每一个base class constructor设定其对象的vptr,使它指向相关的vtbl,构造中的对象就可以严格而正确地编程“构造过程中所幻化出来的每一个class”的对象。constructor执行算法通常如下:
- 在derived class constructor中,“所有virtual base classes”及“上一层base class”的constructor会被调用。
- 对象的vptr初始化,指向相关vtbl。
- 如果存在member initialization list,它们将会在vptr完成设定后展开,以免有一个virtual member function被调用(在Effective C++中我们了解到应当尽量避免在构造和析构函数中调用virtual function)。
- 执行user code。
问题实例
以PVertex constructor为例:1
2
3
4PVertex::PVertex(float x,float y,float z)
:_next(nullptr),Vertex3d(x,y,z),Point(x,y){
if(spyOn) cerr << size() << endl;
}
它很有可能被扩展为:1
2
3
4
5
6
7
8
9
10
11PVertex*
PVertex::PVertex(PVertex* this,bool _most_derived,float x,float y,float z){
if(_most_derived!=false) this->Point::Point(x,y);
//无条件地调用上一层base
this->Vertex3d::Vertex3d(x,y,z);
//设定vptr
this->_vptr_PVertex = _vtbl_PVertex;
this->_vptr_Point_PVertex = _vtbl_Point_PVertex;
if(spyOn) cerr << (*this->_vptr_PVertex[3].faddr)(this) << endl;
return this;
}
尽管上述做法完美解决了虚拟机制的问题,但在实际使用中这种策略并不完美。
下面是vptr必须被设定的两种情况:
- 当一个完整的对象被构造时,如果我们声明一个Point对象,Point constructor必须设定其vptr。
- 当一个subobject constructor调用了一个virtual function时。
如果我们声明了一个PVertex对象,然后由于我们对其base class constructors的最新定义,其vptr将不需要在每一个base class constructor中被设定。解决方法是将constructor分裂为一个完整的object与一个subobject实体,在subobject中,vptr的设定可以省略(就像上一小节末尾提及的那样)。