5.2 继承体系下的对象构造
当定义一个object如下:
T object;
时,实际上会发生什么事情呢?如果T有一个constructor(不论是由user提供或是由编译器合成),它会被调用.这很明显,比较不明显的是,constructor的调用真正伴随了什么?
constructor可能内带大量的隐藏码,因为编译器会扩充每一个constructor,扩充程度视 class T的继承体系而定.一般而言,编译器所做的扩充操作大约如下:
1.记录在member initialization list中的data members初始化操作会被放进constructor的函数本身,并以members的声明顺序为顺序.
2.如果有一个member并没有出现在member initialization list中,但它有一个default constructor,那么该default constructor必须被调用.
3.在那之前,如果 class object有 virtual table pointers,它们必须被设定初值,指向适当的 virtual tables.
4.在那之前,所有上一层的base class constructors必须被调用,以base class 的声明顺序为顺序(与member initialization list中的顺序没关联):
如果base class 被列于member initialization list中,那么任何明确指定的参数都应该传递过去.
如果base class 没有被列于member initialization list中,而它有default constructor(或default memberwise copy constructor),那么就调用它.
如果base class 是多重继承下的第二或后继的base class,那么 this 指针必须有所调整.
5.在那之前,所有 virtual base class constructor必须被调用,从左到右,从最深到最浅:
如果 class 被列与member initialization list中,那么如果有任何明确指定的参数,都应该传递过去.若没有列于list中,而 class 有一个default constructor,也应该调用它.
此外,class 中的每一个 virtual base class subobject的偏移量(offset)必须在执行期可内存取.
如果 class object是最底层(most-derived)的 class,其constructors可能被调用;某些用以支持这个行为的机制必须被放进来.
在这一节中,从"C++语言对classes所保证的语意"这个角度来探讨constructors扩充的必要性,再次以Point为例,并为它增加一个copy constructor,一个copy operator,一个
virtual destructor如下: class Point { public: Point(float x = 0.0, float y = 0.0); Point(const Point &); // copy constructor Point &operator=(const Point &); // copy assignment operator virtual ~Point(); // virtual destructor virtual float z() { return 0.0; } protected: float _x, _y; };
在开始介绍Point的继承体系之前,先看看Line class 的声明和扩充结果,它由_begin和_end两个点构成:
class 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如下:
Line::Line(const Point &begin, const Point &end) : _end(end), _begin(begin) {}
它会被编译器扩充并转换为:
// C++伪代码:Line constructor的扩充 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 operator,以及一个destructor(本例为 virtual),所以Line class 的implicit copy constructor,copy operator和destructor都将有实际功能(nontrivial).
当程序员写下:
Line a;
时,implicit Line destructor会被合成出来(如果Line派生自Point,那么合成出来的destructor将会是 virtual.然而由于Line只是内带Point objects而非继承自Point,所以被合成出来的destructor只是nontrivial而已).在其中,它的member class objects的destructor会被调用(以其构造的相反顺序):
// C++伪代码:合成出来的Line destructor inline void Line::~Line(Line *this) { this->_end.Point::~Point(); this->_begin.Point::~Point(); }
当然,如果Point destructor是 inline 函数,那么每一个调用操作会在调用地点被扩展开来.请注意,虽然Point destructor是 virtual,但其调用操作(在containing class destructor中)会被静态地决议出来(resolved statically).
类似的道理,当一个程序员写下:
Line b = a;
时,implicit Line copy constructor会被合成出来,成为一个 inline public member.
最后,当程序员写下:
a = b;
时,implicit copy assignment operator会被合成出来,成为一个 inline public member.
在产生copy operator的时,需要使用如下的条件语句筛选:
if (this == &rhs) return *this;
在一个由程序员供应的copy operator中忘记检查自我指派(赋值)操作是否失败,是新手极易陷入的一项错误,例如:
// 使用者供应的copy assignment operator // 忘记提供一个自我拷贝时的筛选 String &String::operator=(const String &rhs) { // 这里需要筛选(在释放资源之前) delete []str; str = new char[strlen(rhs.str) + 1]; }
这样一个警告信息是有帮助,"在一个copy operator中,面对自我拷贝缺乏一个筛选操作;但却有一个delete operator对应某个member操作".
虚拟继承 (virtual inheritance)
考虑下面这个虚拟继承(继承自Point)
class 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 的"共享性"的缘故:
// C++伪代码:不合法的constructor扩充内容 Point3d *Point3d::Point3d(Point3d *this, float x, float y, float z) { this->Point::Point(x, y); this->__vptr_Point3d = __vtbl_Point3d; this->__vptr_Point3d__Point = __vtbl_Point3d_Point; this->_z = rhs._z; return this; }
上面的Point3d constructor扩充内容有什么错误?
试着想想以下三种类派生情况:
class 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(不再是Vertex3d)来负责完成"被共享的Point
subobject"的构造.
传统的策略如果要支持"ok,现在将virtual base clas初始化...oh,现在不需要...",会导致constructor中有更多的扩充内容,用以指示 virtual base class constructors应不应该被调用.constructor的函数本身因而必须条件式地测试传进来的参数,然后决定调用或不调用相关的 virtual base class constructors.下面就是Point3d的constructor扩充内容:
// C++伪代码:在virtual base class情况下的constructor扩充内容 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 = __vtbl_Point3d; this->__vptr_Point3d_Point = __vtbl_Point3d__Point; this->_z = rhs._z; return this; }
在更深层的继承情况下,例如Vertex3d,当调用Point3d和Vertex的constructor时,总是会把__most_derived参数设为 false,于是就压制了两个constructors中对Point constructor的调用操作.
// C++伪代码:在virtual base class情况下的constructor扩充内容 Vertex3d *Vertex3d::Vertex3d(Vertex3d *this, bool __most_derived, float x, float y, float z) { if (__most_derived != false) this->Point::Point(x, y); // 调用上一层base classes // 设定__most_derived为false this->Point3d:::Point3d(false, x, y, z); this->Vertex::Vertex(false, x, y); // 设定vptrs // 插入user mode return this; }
这样的策略得以保持语意的正确无误.例如,当定义:
Point3d origin;
时,Point3d constructor可以正确地调用其Point virtual base class subobject.而当定义:
Vertex3d cv;
时,Vertex3d constructor正确地调用Point constructor.Point3d和Vertex的constructor会做每一件该做的事情——对Point的调用操作除外.
vtpr初始化语意学 (The Semantics of the vptr Initialization)
当定义一个PVertex object时,constructors的调用顺序是:
Point(x, y); Point3d(x, y, z); Vertex(x, y, z); Vertex3d(x, y, z); PVertex(x, y, z);
假设这个继承系统中的每一个 class 都定义了一个 virtual function size().该函数负责返回 class 的大小,如果写:
PVertex pv; Point3d p3d; Point *pt = &pv;
那么这个调用操作:
pt->size();
将传回PVertex的大小,而:
pt = &p3d; pt->size();
将传回Point3d的大小.
更进一步,假设这个继承体系中的每一个constructors内带一个调用操作,像这样:
Point3d::Point3d(float x, float y, float z) : _x(x), _y(y), _z(z) { if (spyOn) cerr < "Within Point3d::Point3d()" << " size: " << size() << endl; }
当定义PVertex object时,前述的五个constructors会如何?每一次size()调用会被决议为PVertex::size()吗?或者每次调用会被决议为"当前正在执行的constructor所对应的class"的size()函数实体?
C++语言规则指出,在Point3d constructor中调用的size()函数,必须比决议为Point3d::size()而不是PVertex:size.更一般的,在一个class的constructor或destructor中,经由构造中的对象来调用一个 virtual function,其函数实体应该是在此 class 中有作用的那个.由于各个constructors的调用顺序的缘故,上述情况是必要的.
constructors的调用顺序是:由根源而末端,由内而外.当base class constructor执行时,derived实体还没有被构造处理.在PVertex constructor执行完毕之前,PVertex并不是一个完整的对象;Point3d constructor执行后,只有Point3d subobject构造完毕.
这意味着,当每一个PVertex base class constructors被调用时,编译系统必须保证有适当的size()函数实体被调用,怎样保证这一点?
如果调用操作限制必须在constructor或destructor中直接调用,那么答案十分显然:将每一个调用操作以静态方式决议,千万不要用到虚拟机制.只要是在Point3d constructor中,就明确地调用Point3d::size().
然而如果size()中又调用一个 virtual function,会发生什么事情?这种情况下,这个调用也必须决议为Point3d的函数实体.而在其他情况下,这个调用是纯正的 virtual,必须经由虚拟机制来决定其归属.也就是说,虚拟机制本身必须知道是否这个调用源自于一个constructor中.
另一个可以采取的方法是,在constructor(或destructor)内设立一个标志,指出以静态方式来决议,然后可以以标志值作为判断依据,产生条件式的调用操作.
根本的解决之道是,在执行一个constructor时,必须限制一组 virtual functions候选名单.
想一想,什么是决定一个 class 的 virtual functions名单的关键?答案是
virtual table.Virtual table如何被处理?答案是通过vptr.所以,为了控制一个 class 中有所作用的函数,编译系统只要简单地控制住vptr的初始化和设定操作即可.当然,设定vptr是编译器的责任,任何程序员不必操心此事.
vptr初始化操作应该如何处理?本质而言,这需要视vptr在constructors中"应该在何时被初始化"而定.有三种选择:
1.在任何操作之前.
2.在base base class constructors调用之后,但在程序员供应的码或是在"member initialization list中所列的members初始化操作"之前.
3.在每一件事情发生之后.
答案是2.另外两种选择没有价值.策略2解决了"在class中限制一组virtual function名单"的问题.如果每一个constructor都一直等待直到其base class constructors执行完毕之后才设定其对象的vptr,那么每次它都能够调用正确的 virtual function实体.
令每一个base class constructor设定其对象的vptr,使它指向相关的 virtual table之后,构造中的对象就可以严格而正确地变成"构造过程中所幻化出来的每一个class"的对象.也就是说,一个PVertex对象会先形成一个Point对象,Point3d对象,一个Vertex对象,一个Vertex3d对象,然而才成为一个PVertex对象,在每一个base class constructor中,对象可以与constructor‘s
class的完整对象作比较.对于对象而言,"个体发生学"概括了"系统发生学".constructor的执行算法通常如下:
1.在derived class constructor中,"所有virtual base classes"以及"上一层base class"的constructors会被调用.
2.上述完成后,对象的vptr被初始化,指向相关的 virtual table.
3.如果有member initialization list的话,将在constructors体内扩展开来.这必须在vptr被设定后才进行,以免有一个 virtual member function被调用.
4.最后执行程序员所提供的代码.
例如,已知这个程序员定义的PVertex constructor:
PVertex::PVertex(float x, float y, float z) : _next(0), Vertex3d(x, y, z), Point(x, y) { if (spyOn) cerr << "Within PVertex::PVertex() " << "size: " << size() << endl; }
它很可能被扩张为:
// C++伪代码:PVertex constructor的扩展结果 PVertex *PVertex::PVertex(PVertex *this, bool __most_derived, float x, float y, float z) { // 条件式地调用virtual base constructor 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 << "Within PVertex::PVertex() " << "size: " << (*this->__vptr_PVertex[3].faddr)(this) << endl; // 传回被构造的对象 return this; }
这就完美地解决了所说的有关限制虚拟机制的问题,但是,这真是一个完美的解答?假设Point Constructor定义为:
Point::Point(flaot x, float y) : _x(x), _y(y) {}
Point3d constructor定义为:
Point3d::Point3d(float x, float y, float z) : Point(x, y), _z(z) {}
更进一步,假设Vertex和Vertex3d constructor有类似的定义.是否能够看出解决办法并不完美?
下面是vptr必须被设定的两种情况:
1.当一个完整的对象被构造起来时.如果声明一个Point对象,Point constructor必须设定其vptr.
2.当一个subobject constructor调用了一个 virtual function(不论是直接调用或间接调用)时.
当声明一个PVertex对象,然后由于对其base class constructors的最新定义,其vptr将不再需要在每一个base class constructor中被设定.解决之道是把constructor分裂为一个完整的object实体和一个subobject实体.在subobject实体中,vptr的设定可以忽略.
知道了这些,就能够回答下面的问题:在 class 的constructor的member initialization list中调用该 class 的一个虚拟函数,安全吗?就实际而言,将该函数运行于其class‘s data member的初始化行动中,总是安全的.这是因为,vptr保证能够在member initialization被扩展之前,由编译器正确设定好.但在语意上这可能是不安全的,因为函数本身可能还得依赖未被设立初值的members,所以并不推荐这种做法.然而,从vptr的整体角度来看,这是安全的.
何时需要供应参数给一个base class constructor?这种情况下在"class的constructor的member initialization list中"调用该 class 的虚拟函数,仍然安全吗?不!此时vptr若不是尚未设定好,就是被设定指向错误的 class.更进一步地,该函数所存取的任何class‘s data members一定还没有被初始化.
版权声明:本文为博主原创文章,未经博主允许不得转载。