- 纯虚函数的存在(Presence of a Pure Virtal Function)
- 纯虚函数的存在(Presence of a Pure Virtual Function)
- 虚拟规格的存在(Presence of a Virtual Specification)
- 虚拟规格中const的存在
- 重新考虑class的声明
- 5.1 ”无继承”情况下的对象构造
- 5.2 继承体系下的对象构造
- 5.3 对象复制语意学(Object Copy Semantics)
- 5.4 对象的效能(Object Efficiency)
- 5.5 析构语意学(Semantics of Destruction)
纯虚函数的存在(Presence of a Pure Virtal Function)
- 考虑abstract base class声明: 这个class被设计为一个抽象的base class(其中有pure virtual function,使得Abstract_base不可能有实例),但需要一个显式的构造函数以初始化data member _mumble。如果没有初始化操作,derived class的局部性对象_mumble及那个无法决定初值。
1
2
3
4
5
6
7
8class Abstract_base {
public:
virtual ~Abstract_base() = 0;
virtual void interface() const = 0;
virtual const char* mumble() const { return _mumble; }
protected:
char *_mumble;
};
纯虚函数的存在(Presence of a Pure Virtual Function)
- pure virtual function只能被静态地调用(invoked statically),不能经由虚拟机制调用。而pure virtual destructor,class设计者一定得定义它。因为每个derived class class以及base class destructor。只要缺乏定义,就会导致链接失败。
虚拟规格的存在(Presence of a Virtual Specification)
- 如果决定把Abstract_base::mumble()设计为virtual function,那是糟糕的选择,因为其函数定义内容并不与类型有关,因而几乎不会被后继的derived class改写。
虚拟规格中const的存在
- 决定要给virtual function是否需要const,是一件繁琐的事情。如果声明为const,意味着subclass实例可能被无穷次数地使用。不把函数声明为const,意味和函数不能获得一个const reference或const pointer。头大的是,声明函数为const,才发现derived instance必须修改某一data member。解决办法就是不再用const就是了。
重新考虑class的声明
- 综上所述,这才是比较适当的一种设计:
1
2
3
4
5
6
7
8
9class Abstract_base {
public:
virtual ~Abstract_base(); // nonpure virtual
virtual void interface() = 0; // nonconst
const char* mumble() const { return _mumble; } // nonvirtual
protected:
Abstract_base(char *pc =ss 0);
char *_mumble;
}
5.1 ”无继承”情况下的对象构造
考虑程序片段:
1
2
3
4
5
6
7
8
9
10
11Point global; //1
//2
Point foobar() //3
{ //4
Point local; //5
Point *heap = new Point; //6
*heap = local; //7
// ... stuff ... //8
delete heap; //9
return local; //10
} //11L1、L5、L6分别为:global内存配置、local内存配置和heap内存配置。一个object的声明,是该object的一个执行期属性。local object的生命从L5定义开始,L10为止。global object的生命和这个程序的生命相同。heap object的生命从被new运算符配置出来开始,到delete运算符摧毁结束。
下面是Point的第一次声明,写成C程序。这是一种Plain OI’ Data声明形式:1
2
3
4
5typedef struct
{
float x, y, z;
Point;
}如果用C++来编译这代码,编译器会为Point声明一个trivial default constructor、一个trivial destructor、一个trivial copy constructor,以及一个trivial copy assignment operator。但实际上,编译器会分析这个声明,并贴上POD标签。
当编译器遇到这样的定义:
1
Point global; //1
观念上Point的trival constructor和destructor会被产生并调用,constructor在程序起始(startup)被调用,而destructor在程序的exit()处被调用。然而事实上trivial members要么没定义,要么没被调用。
在C中,global被视为一个临时性的定义,因为它没有显式的初始化操作。它可以在程序中发生多次。实例会被折叠起来,只留下一个,放在程序data segment中。这块空间称为BSS,这是Block Started by Symbol缩写。
C++并不支持临时性定义。虽然它可以判断class object或是POD。global在C++中被视为完全定义(会阻止多个定义)。C和C++的差异是,BSS data segment在C++中相对不重要。所有的全局变量都被以“初始化过的数据”来对待。
至于foobar()函数的L5,既没有构造也没有被析构,不过可能没有经过初始化就会成BUG(L7)。至于heap object:1
2
3
4
5Point *heap = new Point; //6
// 转化
Point *heap = __new(sizeof(Point));
// 有初始化过就没问题
*heap = local; //7事实上L7会产生编译警告,观念上,这样的指定操作会触发trivial copy assignment operator做拷贝搬运操作。而实际上object是POD,所以assignment只是像C那样纯粹bitwise搬运。
抽象数据类型(Abstract Data Type)
- 以下是Point声明,提供了完整封装性,但没有提供virtual function: 这里还是三个连续的float,不论private或public存取层或是member function都不会占用额外的对象空间。没有定义copy constructor或copy operator,因为有default bitwise semantics足够了,也不需要destructor。对于实例:
1
2
3
4
5
6
7
8
9class Point {
public:
Point(float x = 0.0, float y = 0.0, float z = 0.0) : _x(x), _y(y), _z(z) {}
// no copy constructor, copy operator
// or destructor defined ...
// ...
private:
float _x, _y, _z;
};有了default constructor,global被定义在全局范畴,初始化操作将延迟到程序启动(startup)。1
Point global; // Point::Point(0.0, o.0, 0.0);
如果要将class中的所有成员设定常量初值,给予一个explicit initialization list会比较有效率(相比constructor的inline expansion而言)。但它有三项缺点:- 只有当class members都是public,才会奏效。
- 只能指定常量,因为在编译器就能评估求值。
- 由于编译器并不自动施行,初始化行为可能性很高。
为继承做准备
- 第三个Point声明,为继承性质和某些操作的动态决议(dynamic resolution)做准备,目前限制z成员做存取操作: virtual functions的导入在这里附带一个virtual destructor的声明在这个例子里,并无好处。除此之外,每个class object多负担一个vptr之外,virtual function的导入也引发编译器对Point class产生的膨胀作用:
1
2
3
4
5
6
7
8
9
10class Point {
public:
Point(float x = 0.0, float y = 0.0) : _x(x), _y(y) {}
// no destructor, copy constructor, or
// copy operator defined ...
virtual float z();
// ...
protected:
float _x, _y;
};- 定义的constructor附带了一些代码,以便vptr初始化:
1
2
3
4
5
6
7
8
9
10
11// C++伪码
Point* Point::Point(Point *this, float x, float y) : _x(x), _y(y)
{
// 设定object的vptr
this->__vptr_Point = __vtbl__Point;
// 扩展member initialization list
this->_x = x;
this->_y = y;
// 传回this
return this;
} - 合成一个copy constructor和copy assignment operator,而且操作不是trivial(implicit destructor仍然是)。如果Point object被初始化或以derived class object赋值,那么以为基础(bitwise)的操作可能对vptr带来非法设定: 编译器在优化状态会把object内容连续拷贝到另一个object,而不是精准memberwise。L7的memberwise赋值操作可能出发copy assignment operator的合成,及调用inline expansion(行内扩张):以this取代heap,以rhs取代local。
1
2
3
4
5
6
7
8
9// C++伪码
// copy constructor 内部合成
inline Point* Point::Point(Point*this, const Point &rhs)
{
// object的vptr
this->__vptr_Point = __vtbl__Point;
// copy
return this;
}
戏剧性的冲击在L10。由于copy constructor的出现,foobar()可能转化为:如果NRV优化,转化为:1
2
3
4
5
6
7
8
9
10
11
12Point foobar(Point &__result)
{
Point local;
local.Point::Point(0.0, 0.0);
// heap
// copy constructor
__result.Point::Point(local);
// local destructor
// Point::derstructor
// local.Point::~Point();
return
}1
2
3
4
5
6Point foobar(Point &__result)
{
__result.Point::Point(0.0, 0.0);
// heap
return;
}
- 定义的constructor附带了一些代码,以便vptr初始化:
5.2 继承体系下的对象构造
当定义一个object:
1
T object;
如果T有constructor,它会被调用。除此之外,constructor调用伴随了什么?constructor可能内含大量隐藏码,因为编译器会扩充每个constructor:
- 在member initialization list中的data members初始化操作会被放进constructor函数本体,以声明顺序为顺序。
- 如果member没有在member initialization list中,但这个member有default constructor,那么被调用。
- 在那之前,如果class object有vptr,必须被设定初值指向适当vtbls。
- 在那之前,base class constructors必须被调用,以base class声明顺序为顺序。
- 如果base class被列入member initialization list中,那么任何显式参数都传递进去。
- 如果base class没有被列入member initialization list中,而它有default constructor(或default memberwie copy constructor),那么调用它。
- 如果base class是多层继承下的第二或后继base class,那么调整this指针。
- 在那之前,所有virtual base class constructors必须被调用,从左到右,从深到浅:
- 如果class被列入member initialization list中,那么任何显式指定的参数,都传递进去。如果没有列入list中,而class有default constructor,调用它。
- class中的每个virtual base class subobject的偏移地址(offset)必须在执行期可被存取。
- 如果class object是最底层(most-derived)的class,其constructors可能被调用;用以支持者行为的机制必须放出来。
以Point为例,探讨constructors扩充的必要性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class 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 derstructor
virtual float z() { return 0.0; }
// ...
protected:
float _x, _y;
};
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();
// ...
};。。。。。。。。
虚拟继承(Virtual Inheritance)
- 考虑下面虚拟继承,继承自Point: 传统的constructor扩充并没有用,因为virtual base class的共享性之故:
1
2
3
4
5
6
7
8
9
10
11class 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) {}
~Point3();
Point3d& operator=(const Point3d&);
virtual float z() { return _z; }
// ...
protected:
float _z;
};看出什么错误了吗?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_Piont3d = __vtbl_Point3d;
this->__vptr_Point3d__Point = __vtbl_Point3d_Point;
this->_z = rhs._z;
return this;
}Vertex的constructor必须调用Point constrcutor。然而当Point3d和Vertex为Vertex3d的subobjects时,则调用操作不一定发生。1
2
3
4
5
6graph BT
Point3d -- virtual --- Point
Vertex -- virtual --- Point
Vertex3d -- public --- Point3d
Vertex3d -- public --- Vertex
pVertex -- public --- Vertex3dVertex3d调用Point3d和Vertex的constructor时,会把__most_derived参数设为false,于是就压制了对两个constructors中对Point constructor的调用操作。1
2
3
4
5
6
7
8
9
10// Point3d的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 = ths._z;
return this;
}这个策略得以保持语义正确。当定义一个Point3d时,Point3d constructor可以正确调用Point virtual base class subobject。当定义Vertex3d时时,Vertex3d constructor正确调用Point constructor。Point3d和Vertex的constructor不会对Point调用。这种把一个constructor分裂为二的做法,可以带来速度的提升。??1
2
3
4
5
6
7
8Vertex3d* Vertex3d::Vertex3d(Vertex3d *this, bool __most_derived, float x, float y, float z)
{
if (__most_derived != false)
this->Point::Point(x, y);
this->Point3d::Point3d(false, x, y, z);
this->Vertex::Vertex(false, x, y);
return this;
}
vptr初始化语意学(The Semantics of the vptr Initialization)
- 当我们定义PVertex object时,constructors的调用顺序是:
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(),返回class的大小。而这个继承体系中的每个constructors内含调用操作: 在一个class的constructor(和destructor)中,经由构造中的对象来调用virtual function,其函数实例是在此class中有作用的那个。如果调用操作限制必须在constructor(或destructor)中直接调用,那么答案十分明显;将每个调用以静态方式决议它,不要用到虚拟机制。如果是在Point3d constructor中,就显式调用Point3d::size()。
1
2
3
4
5Point3d::Point3d(float x, float y, float z) : _x(x), _y(y), _z(z)
{
if (spyOn)
cerr << "Within Point3d::Piont3d()" << "size: " << size() << endl;
} - 如果size()之中又调用一个virtual function,这个调用也必须决议为Point3d的函数实例。因此在执行constructor时,必须限制一组virtual functions候选名单。为了控制class中有所作用的函数,编译系统需要控制住vptr的初始化和设定操作,vptr初始化操作怎么处理,得视vptr在constructor中应该在什么时候被初始化而定。 在base class constructors调用操作之后,在member iniaialization list中members初始化操作前。它解决了class中限制一组virtual functions名单的问题。如果每个constructor都一直等待其base class constructors执行完毕之后才设定对象的vptr,那么每次都能调用正确的virtual function实例。
constructor的执行算法通常如下:- 在derived class constructor中,所有virtual base classes及上层base class的constructors会被调用。
- **对象的vptr(s)会被初始化,指向相关virtual table(s)**。
- 如果有member initialization list,将在constructor内扩展开来。这在vptr被设定之后才做,以免virtual member function被调用。
- 最后才执行程序员所提供的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30PVertex::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* 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;
}
- 下面是vptr必须被设定的两种情况:
- 当一个完整的对象被构造起来时。如果我们声明一个Point对象,则Point constructor必须设定其vptr。
- 当一个subobject constructor调用一个virtual function(不论是直接调用还是间接调用)时。
p218
5.3 对象复制语意学(Object Copy Semantics)
当我们设计一个class,并以一个class object指定给另一个class object时,我们有三种选择:
- 什么都不做,施行默认行为。
- 提供一个explicit copy assignment operator。
- 显式地拒绝把一个class object指定给另一个class object。
当选择不准将一个class object指定给另一个class object时,只要把copy assignment operator声明为priavte,并且不提供其定义就可以了。设为private,就不再允许于任何地点(除了在member functions以及该class的friends之中)做赋值(assign)操作。
需要验证copy assignment operator的语意,利用Point class来帮助讨论:
1
2
3
4
5
6
7class Piont {
public:
Point(float x = 0.0, float y = 0.0);
// ... 没有virtual function
protected:
float _x, _y;
};对于默认行为是否足够,如果要支持的只是一个简单的拷贝操作,那么默认行为不但足够而且有效率,而且没有理由再自己提供一个copy assignment operator。
如果不对Point供应一个copy assignment operator,光是以来memberwise copy,编译器不会产生实例。编译器不会产生出一个实例,因为class有了bitwise copy语义,所以implicit copy assignment operator被视为毫无用处,也不会被合成出来。copy assignment operators并不表示bitwisecopy semantics是nontrivial,只有nontrivial instances才会被合成出来。因此,对于Point class,这样的赋值操作:1
2
3Point a, b;
...
a = b;由bitwise copy完成,期间并没有copy assignment oeprator被调用。注意,我们还是可能提供一个copy constructor,为的是把name return value(NRV)优化打开。copy constructor的出现不意味着也也要提供一个copy assignment operator。
如果该operator在继承之下呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24inline Point& Point::operator=(const Point &p)
{
_x = p._x;
_y = p._y;
return *this;
}
// 虚拟继承
class Point3d : virtual public Point {
public:
Point3d(float x = 0.0, float y = 0.0, float z = 0.0);
// ...
protected:
float _z;
};
// 如果没有定义copy assignmnet operator,根据合成规则的第二项和第四项。合成的东西看起来这样:
inline Point3d& Point3d::operator=(Point3d *const this, const Piont3d &p)
{
// call base class
this->Point::operator=(p);
// memberwise copy the derived class members
_z = p._z;
return *this;
}它缺乏一个member assignment list(平行于member initialization list的东西)。缺少copy assignment list,看起来是小事,但如果没有它,编译器一般就没有办法压抑上一层base class的copy operators被调用。例如Vertex copy operator,Vertex虚拟继承自Point:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// class Vertex : virtual public Point
inline Vertex& Vertex::operator=(const Vertex &v)
{
this->Point::operator=(v);
_next = v._next;
return *this;
}
// 从Point3d和Vertex派生Vertex3d,下面是其copy assignment operator
inline Vertex3d& Vertex3d::operator=(const Vertex3d &v)
{
this->Point::operator=(v);
this->Point3d::operator=(v);
this->Vertex::oeprator=(v);
...
}编译器怎么能在Point3d和Vertex的copy assignment operators中压抑Point的copy assignment operators呢?有一种方法可以保证most-derived class会(完成)virtual base class subobject的copy行为,就是在derived class的copy assignment operator函数实例的最后,显式调用oeprator:
1
2
3
4
5
6
7
8
9inline Vertex3d& Vertex3d::operator=(const Vertex3d &v)
{
this->Point3d::oeprator=(v);
this->Vertex::oeprator=(v);
// must place this last if your compiler does
// not suppress intermediate class invocations
this->Point::operator=(v);
...
}它不能够省略subobjects的多重拷贝,但可以保证语意。建议尽可能不要允许一个virtual base class的拷贝操作,甚至一个比较奇怪的建议:不要在任何virtual base class中声明数据。
5.4 对象的效能(Object Efficiency)
- 测试。
5.5 析构语意学(Semantics of Destruction)
如果class没有定义destructor,那么只有在class内含的member object(抑或是class的base class)拥有destructor的情况下,编译器才会自动合成一个来。当从父类派生子类(即使是一种虚拟派生关系)时,如果没有声明一个estructor,编译器就没有必要合成一个destructor。没有任何理由说在delete一个对象之前得先讲内容清除干净。在一个对象的生命之前,没有任何class使用层面的程序操作是必要的,因此也就不需要要给destructor。
一个由程序员定义的destructor被扩展的方式类似constructors被扩展的方式,但顺序相反:
- destructor的函数本体现在被执行,也就是说vptr会在程序员的代码执行前被重设(reset)。
- 如果class拥有member classs objects,而后者会拥有destructors,那么它们会以生命顺序的相反顺序被调用。
- 如果object内含一个vptr,那么首先重设(reset)相关的vtbl。指向适当的base class的vtbl。
- 如果由任何直接的(上一层)nonvirtual base classes拥有destrucotr,它们会以其声明顺序的相反顺序被调用。
- 如果有任何virtual base classes拥有destructor,那么也会以原来的构造顺序的相反顺序被调用。
跟constructor一样,对destructor的一种最佳实现策略就是维护两份destrucotr实例:
- 一个complete object实例,先设定好vptr(s),并调用virtual base classes destructors。
- 一个base class subobject实例:除非在destructor函数中调用一个virtual function,否则不会调用virtual base class destructors并设定vptr。
一个object的声明结束于其destructor开始执行之时。