继承与 Data Member(3)

虚拟继承
多重继承的一个语义上的副作用就是, 它必须支持某种形式的 shared subobject 继承, 一个典型的例子是最早的 iostream library:

// pre-standard iostream implement
class ios{...};
class istream: public ios{...};
class ostream: public ios{...};
class iostream: 
    public istream, public ostream{...};

以上的代码有冗余, 因为无论是 istream 还是 ostream, 都内含一个 ios subobject, 然而在 iostream 的对象布局中, 我们只需要一份 ios subobject 就好, 所以在语言层面的解决方法就是虚拟继承。
虚拟继承的实现难度比较高, 难点在于要找到一个足够有效的方法, 将 istream 和 ostream 各自维护的 ios subobject 折叠成为一个单一的 ios subobject, 并且还可以保存 base class 和 derived 的指针(以及 reference) 之间的多态指定操作。
一般实现的方法如下所述, class 如果含有一个或多个 virtual base class subobject, 像 istream 那样, 将被分割成两部分: 一个不变局部和一个共享局部。
不变局部中的数据, 不管后继如何衍化, 总是有固定的 offset(从 object 的开头算起), 所以这一部分数据可以被直接存取。 至于共享局部, 所表现的就是 virtual base class subobject, 这一部分的数据,其位置会因为每次的派生操作而变化, 所以它们只能被间接存取, 各家编译器实现技术之间的差异就在于间接存取的方法不同, 以下是三种主流策略:

//虚拟继承的层次结构
class Point2d
{
public:
    ...
protectedfloat _x, _y;
};

class Vertex: public virtual Point2d
{
public:
    ...
protected:
    Vertex *next;
};

class Point3d: public virtual Point2d
{
public:
    ...
protected:
    float _z;
};

class Vertex3d:
    public Vertex, public Point3d
{
public:
    ...
protected:
    float mumble;
};

一般的布局策略是先安排好 derived class 的不变部分, 然后再建立起共享部分。
然而,这中间存在一个问题:如何能够存取 class 的共享部分呢? cfront 编译器会在每一个 derived class object 中安插一些指针,每个指针指向一个 virtual base class。要存取继承来的 virtual base class members, 可以使用相关指针间接完成, 举个例子:

void Point3d::
operator+=(const Point3d &rhs)
{
    _x += rhs._x;
    _y += rhs._y;
    _z += rhs._z;
}

//在 cfront 的策略下, 这个运算符会被转换为
_vbcPoint2d->_x += rhs._vbcPoint2d->_x;    //vbc 意为 virtual base class
_vbcPoint2d->_y += rhs._vbcPoint2d->_y;
_z += rhs._z;

//一个 derived class 和一个 base class 的实例转换
Point2d *p2d = pv3d;
//在 cfront 实现模型之下
Point2d *p2d = pv3d? pv3d->_vbcPoint2d : 0;

这样的模型有两个缺点:
1. 每一个对象必须针对其每一个 virtual base class 背负一个额外的指针, 然而理想上我们却希望 class object 有固定的负担, 不因为其 virtual base class 的数目而有变化。
2. 由于虚拟继承的串联的加长, 导致间接存取层次的增加。这个意思是, 如果我有三层虚拟衍化, 我就需要三次间接存取。 但是理想上我去希望有固定的存取时间, 不会因为虚拟衍化的深度而改变。
我总结了一下。 其实就是这个策略开销不稳定
许多的编译器都使用 cfront 的院士实现模型来解决第二个问题, 它们经由拷贝操作得到的所有的 nested virtual class 指针, 放到 derived class object 之中, 这就解决了固定存储时间的问题。 虽然代价是空间。
而第一个问题, 一般而言有两种解决办法。
Microsoft 编译器引入所谓的 virtual base class table。 每一个 class object 如果有一个或多个 virtual base classes, 就会由编译器安插一个指针, 指向 virtual base class table。 至于真正的 virtual base class 指针, 就放在该表格中, 如图:

第二个解决办法是 Bjarne 比较喜欢的办法, 是在 virtual function table 中放置 virtual base ckass 的 offset(而不是地址)。 下图为其实现模型:

 

在 Sun 的编译器中, virtual function table 可经由正值和负值来索引。 如果是正值, 那就是索引到 virtual functions; 如果是负值, 则是索引到 virtual base class offsets。 在这样的策略之下, Point3d 的 operator+= 运算符必须被转换为以下形式:

//其中 _vptr_Point3d[-1] 保存的就是 object 的起始地址到其 vptr 的偏移量。
//相加得到的值是相应的 base class object 指针, 参照图较好理解
(this + _vptr_Point3d[-1])->_x += 
    (&rhs + rhs._vptr_Point3d[-1])->_x;
(this + _vptr_Point3d[-1])->_y += 
    (&rhs + rhs._vptr_Point3d[-1])->_y;
_z += rhs._z;

在这个策略之下, 对继承而来的 members 做存取操作, 成本会比较昂贵, 不过该成本已被分散至对 member 的使用上, 属于局部性成本。 对于 Derived class 实体和 base class 实体之间的转换操作, 例如:

Point2d *p2d = pv3d;
//转换的操作
Point2d *p2d = pv3d? pv3d + pv3d->_vptr_Point3d[-1]:0;

上述每一种方法都是一种实现模型, 都是用来解决存取 shared subobject 内的数据(其位置会随每次派生操作而变化) 所引发的问题。 由于对 virtual base class 的支持带来额外的负担以及高度复杂性, 每一种实现模型多少有一点不同, 而且我想还会随着时间而进化。
经由一个非多态的 clas object 来存取一个继承而来的 virtual bass class 的 member, 像这样:

Point3d origin;
...
origin._x;

可以被优化为一个直接存取操作, 就好像一个经由对象调用的 virtual function 调用操作可以在编译时期被决议完成一样。在这次存取和下次存取之间, 对象的类型不可以改变, 所以 virtual base class subobjects 的位置会变化的问题在这之下就不存在了。
一般而言, virtual base class 最有效的一种运用形式就是: 一个抽象的 base class, 没有任何 data members。(我觉得那不就是一个接口吗?)

posted @ 2014-11-21 13:04  wu_overflow  阅读(178)  评论(0编辑  收藏  举报