多重继承下 Virtual Function 的语意

在多重继承中支持 virtual function, 其复杂度围绕在第二个及后继的 base classes 上, 以及必须在执行期调整 this 指针这一点, 以以下的 class 体系为例:

class Base1
{
public:
    Base1();
    virtual ~Base1();
    virtual void SpeakClearly();
    virtual Base1* Clone() const;
protected:
    float data_base1;
};

class Base2
{
public:
    Base2();
    virtual ~Base2();
    virtual void Mumble();
    virtual Base2* Clone() const;
protected:
    float data_base2;
};

class Derived: public Base1, public Base2
{
public:
    Derived();
    virtual ~Derived();
    virtual Derived* Clone() const;
protected:
    float data_derived;
};

在以上的体系中, Derived 支持 virtual functions 的困难度统统落在 Base2 subobject 身上. 共有三个问题需要解决:
1. virtual destructor
2. 被继承下来的 Base2::Mumble()
3. 一组 Clone() 实体.
咱们来一一解决!
首先, 把从 heap 中配置的 Derived 对象的地址, 指定给一个Base2 指针:

Base2 *pbase2 = new Derived;

新的 Derivec 对象地址必须调整, 以指向其 Base2 subobject, 编译时会产生以下代码:

//转移地址以支持第二个 base class
Derived *temp = new Derived;
Base2 *pbase2 = temp ? temp + sizeof(Base1) : 0;

如果没有这样的调整, 指针的任何非多态运用都将失败, 如下:

//pbase2 被指定一个Derved 对象, 应该没问题
pbase2->data_base2;

当程序员要删除 pbase2 所指的对象时:

//必须首先调用正确的 virtual destructor 函数实体
//然后施行 delete 运算符
//pbase2 可能需要被再一次调整, 以指出完整对象的起点
delete pbase2;

指针必须被再一次调整, 以求再一次指向 Derived 对象的起始处(推测它还指向 Derived 对象), 然而上述的 offset 加法却不能在编译时期直接确认, 因为 pbase2 所指的对象只有在执行期才能确定.
一般的规则是, 经由指向第二或后继的 base class 的指针(或 reference) 来调用 derived class virtual function:

Base2 *pbase2 = newDerived;
...
delete pbase2;

该调用所连带的必要的 this 指针调整操作必须在执行期完成, 也就是说, offset 的大小, 以及把 offset 加到 this 指针上头的那一小段代码必须由编译器在某个地方插入, 问题是插哪个地方?
本贾尼原先实施于 cfront 编译器中的方法是将 virtual table 扩大, 使它容纳此处所需的 this 指针, 调整相关事物. 每一个 virtual table slot 不再只是一个指针, 而是一个聚合体, 内含可能的 offset 及地址. 于是 virtual function 的调用操作由:

(*pbase2->vptr[1])(pbase2);
改变为:
(*pbase2->vptr[1].faddr)
    (pbase2 + pbase2->vptr[1].offset);

这个方法的缺点就是连带惩罚了所有的 virtual function 调用操作,不管他们是否需要 offset 的调整. 所说的处罚包括 offset 的额外存取及加法, 以及每个 virtual table slot 大小的改变.
比较有效率的方法是利用所谓的 thunk.Thunk 技术就是一小段 assembly 码(汇编语言), 用来:
1. 以适当的 offset 值调整 this 指针
2. 跳到 virtual function 去
例如, 经由一个Base2 指针调用 Derived destructor, 其相关的 tunk 可能看起来是这个样子:

//虚拟 C++ 码
pbase2_dtor_thunk:
    this += sizeof(basel);
    Derived::~Derived(this);

本贾尼并不是不知道 thunk 技术, 问题是 thunk 只有以 assembly 码完成才有效率可言, 由于 cfront 使用 C 作为其程序代码产生语言, 所以无法提供一个有效率的 tunk 编译器.
Thunk 技术允许 virtual table slot 继续内含一个简单的指针, 因此多重继承不需要任何空间上的额外负担, slot 中的地址可以直接指向 virtual function, 也可以指向一个相关的thunk(如果需要调整 this指针的话). 于是, 对于那些不需要调整 this 指针的virtual function 而言, 也就不需要承载效率上的额外负担.
调整 this 指针的第二个额外负担就是, 由于两种不同的可能:
1. 经由 derived class 或第一个 base class 调用
2. 经由第二个或后继的 base class 调用
同一函数在 virtual table 可能需要多笔对应的 slots, 例如:

Base1 *pbase1 = new Derived;
Base2 *pbase2 = new Derived;

delete pbase1;
delete pbase2;

虽然两个 delete 操作导致相同的 Derived destructor, 但他们需要不同的 virtual table slots:
1. pbase1 不需要调整 this 指针(因为 Base1 是最左端的 base class 之故, 它已指向 Derived 对象的起始处, 可见先进门三天就是大). 其 virtual table slot 需放置真正的 destructor 地址.
2. pbase2 需要调整 this 指针. 其 virtual table slot 需要相关的 thunk 地址.
在多重继承下, 一个 derived class 内含 n - 1 个额外的 virtual tables, n 表示其上一层 base classes的数目(所以单一继承不会有额外的 virtual tables). 对于本例而言, 会有两个 virtual tables 被编译器产生出来:
1. 一个主要实体, 与 Base1(最左端 base class 正室) 共享.
2. 一个次要实体, 与 Bsae2(第二个 base class 侧室) 有关.
针对每一个 virtual tables, Derived 对象中有对应的 vptr. 下图说明了这一点, vptrs 将在 constructor 中被设立初值(经由编译器所产生的码).

用来支持一个class 拥有多个 virtual tables 的传统方法是, 将每一个 tables 以外部对象的形式产生出来并给与独一无二的名称, 例如 Derived 所关联的两个 tables 可能有这样的名称:

vtbl__Derived;        //主要表格
vtbl__Base2__Derived; //次要表格

于是当你将一个Derived 对象地址指定给一个 Base1 指针给一个 Base1 指针或 Derived 指针时, 被处理的 virtual table 是主要表格, 而当你将一个 Derived 对象地址指定给一个 Base2 指针时, 被处理的 virtual table 是次要表格 vtbl__Base2_Derived.
之前的博客中曾说过, 有三种情况, 第二或后继的 base class 会影响对 virtual function 的支持.
1. 第一种情况是通过一个指向第二个 base class 的指针, 调用 derived class virtual function如:

Base2 *ptr = new Derived;
//调用 Derived::~Derived
//ptr 必须被向后调整sizeof(Base1) 个 bytes
delete ptr;

从上图之中可以看到这个调用操作的重点: ptr 指向 Derived 对象中的 Base2 subobject; 为了能够正确执行, ptr 必须调整指向 Derived 对象的起始处.
2. 第二种情况是第一种情况的变化, 通过一个指向 derived class 的指针, 调用第二个 base class 中继承来的 virtual function. 在此情况之下, derived class 指针必须再次调整, 以指向第二个 base subobject, 如:

Derived *pder = new Derived;
//调用 Base2::Mumble()
//pder 必须被向前调整 sizeof(Base1) 个 bytes
pder->Mumble();

3. 第三种情况发生在一个语言的扩充性质之下: 允许一个 virtual function 的返回值类型有所变化, 可能是 base type,也可能是 publicly derived type. 这一点可以通过 Derived::Clone()函数实体来说明. Clone 函数的 Derived 版本传回一个 Derived clas 指针, 默默地改写了它的两个 base class 函数实体. 当我们通过指向第二个 base class 的指针来调用 Clone() 时, this 指针的 offset 问题于是产生:

Base2 *pb2_1 = new Derived;
//调用 Derived* Derived::Clone()
//返回值必须被调整, 以指向 Base2 subobject
Base2 *pb2_2 = pb2_1->Clone();

当进行 pb1->Clone() 时, pb2_1 会被调整指向 Derived 对象的起始地址,于是 Clone() 的 Derived 版会被调用: 它会传回一个指针, 指向新的 Derived 对象; 该对象在被指定给 pb2_2 之前, 必须先经过调整,以指向 Base2 subobject.
Microsoft 以所谓的 address points 来取代 thunk 策略: 将用来改写别人的那个函数(说人话就是 overriding function) 期待获得的是引入该 virtual function 的 class (而非 derived class) 的地址. 这就是该函数的 address point.

时间: 2024-10-14 00:53:14

多重继承下 Virtual Function 的语意的相关文章

虚拟继承下 Virtual Function 的语意

考虑下面的 virtual base class 派生体系: class Point2d { public: Point2d(float = 0.0, float = 0.0); virtual ~Point2d(); virtual void Mumble(); virtual float Z(); //... protected: float _x, _y; }; class Point3d:public virtual Point2d { public: Point3d(float = 0

C++单一、多重继承下的内存模型

一:C++单一继承下的内存模型: a).最简单的一种单一继承内存模型:基类及派生类中无virtual function member: #include <iostream> class Base { public: Base(char _x = '\0') :m_x(_x) {} ~Base() {} private: char m_x; }; class Derived :public Base { public: Derived(char _x = '\0',int _y = 0,int

OD: Memory Attach Technology - Off by One, Virtual Function in C++ &amp; Heap Spray

Off by One 根据 Halvar Flake 在"Third Generation Exploitation"中的描述,漏洞利用技术依攻击难度从小到大分为三类: 1. 基础的栈溢出利用,可以利用返回地址轻松劫持进程,植入 shellcode,如对 strcpy.strcat 等函数的攻击. 2. 高级栈溢出利用.栈中有限制因素,溢出数据只能淹没部分 EBP,但无法淹没返回地址,不能获得 EIP 控制权.经典例子是对 strnpy 函数误用时产生的 off by one 漏洞.

多重继承下的类型转换

主要解释强制类型转换的影响.因为static_cast会在编译期间检测,dynamice_cast会在运行时检测. #include <iostream> #include <hash_map> using namespace std; class I1 { public: virtual void vf1() { cout << "I'm I1:vf1()" << endl; } }; class I2 { public: virtua

C++ 实用泛型编程之 虚拟函数(C++ virtual function)杂谈

一 C++虚拟函数(C++ virtual function)杂谈 我们在编程的时候,经常会遇到这样的情况,假设有两个对象,你要在函数中分别调用它们的OnDraw方法,我们以前的做法一般是这样的. void f(int iType) { switch(iType) { case 1: //CCircle OnDraw break; case 2: //CRectangle OnDraw break; } } 这种方法当然能解决我们的问题,但是如果有新的类型要增加,它就必须要往下加代码才行了,这样

C# abstract function VS virtual function?

An abstract function has to be overridden while a virtual function may be overridden. Virtual functions can have a default /generic implementation in the base class. An abstract function can have no functionality. You're basically saying, any child c

Template 和 virtual function

Template和virtual function是两种不同类型的多态. Template的多态是在编译期决定的,而virtual function的多态是在运行时决定的. 从应用形式上看,Template是发散式的,让相同的实现代码应用于不同的场合:virtual function是收敛式的,让不同的代码用于相通的场合. 从思维方式上看,Template是泛型式编程风格,看重算法的普适性:virtual function是对象式编程风格,看重的是接口和实现的分离度.

effective c++ 条款9 do not call virtual function in constructor or deconstructor

在构造函数中不要调用virtual函数,调用了也不会有预期的效果. 举个例子 class Transaction { public: Transaction() { log(); } virtual void log() =0; } class BusinessTransaction: public Transaction { public: virtual void log() { ;//log something here } } BusinessTransaction b_trx; b_t

[C++] Pure Virtual Function and Abstract Class

Pure Virtual Function Abstract Class