3

当使用多重继承时,C++ 必须维护几个 vtable,这导致公共基类具有“多个视图”。

这是一个代码片段:

#include "stdafx.h"
#include <Windows.h>

void dumpPointer( void* pointer )
{
    __int64 thisPointer = reinterpret_cast<__int64>( pointer );
    char buffer[100];
   _i64toa( thisPointer, buffer, 10 );
    OutputDebugStringA( buffer );
    OutputDebugStringA( "\n" );
}

class ICommonBase {
public:
    virtual void Common() = 0 {}
};

class IDerived1 : public ICommonBase {
};

class IDerived2 : public ICommonBase {
};

class CClass : public IDerived1, public IDerived2 {
public:
    virtual void Common() {
        dumpPointer( this );
    }
    int stuff;
};

int _tmain(int argc, _TCHAR* argv[])
{
    CClass* object = new CClass();
    object->Common();
    ICommonBase* casted1 = static_cast<ICommonBase*>( static_cast<IDerived1*>( object ) );
    casted1->Common();
    dumpPointer( casted1 );

    ICommonBase* casted2 = static_cast<ICommonBase*>( static_cast<IDerived2*>( object ) );
    casted2->Common();
    dumpPointer( casted2 );

    return 0;
}

它产生以下输出:

206968 //CClass::Common this
206968 //(ICommonBase)IDerived1::Common this
206968 //(ICommonBase)IDerived1* casted1
206968 //(ICommonBase)IDerived2::Common this
206972 //(ICommonBase)IDerived2* casted2

这里casted1casted2不同的值,这是合理的,因为它们指向不同的子对象。在调用虚函数时,基类的转换已经完成,编译器不知道它最初是一个最派生的类。仍然每次都是一样的它是如何发生的?

4

4 回答 4

3

当转换为不同的类型时,字段的偏移量以及 vtable 中的条目必须位于一致的位置。将 aICommonBase*作为参数的代码不知道您的对象实际上是一个IDerived2. 然而它仍然应该能够取消引用->foo或调用虚拟方法bar()。如果这些不在无法工作的可预测地址。

对于单继承的情况,这很容易做到。如果Derived继承自Base,你可以说偏移量 0Derived也是偏移量 0 Base,唯一的成员Derived可以在 的最后一个成员之后Base。对于多重继承,显然这是行不通的,因为 的第一个字节Base1也不能是Base2. 每个人都需要自己的空间。

因此,如果您有这样一个从两个继承的类(调用它Foo),编译器可以知道对于 type FooBase1部分从偏移量 X 开始,Base2部分从偏移量 Y 开始。当转换为任一类型时,编译器可以将适当的偏移量添加到this.

Foo当调用由 提供实现的实际虚方法时Foo,它仍然需要指向对象的“真实”指针,以便它可以访问其所有成员,而不仅仅是基的特定实例Base1Base2. 因此this仍然需要指向“真实”的对象。

请注意,它的实现细节可能与描述的不同,这只是对问题存在原因的高级描述。

于 2009-11-17T07:59:01.223 回答
3

当在虚函数调用中使用多重继承时,对虚函数的调用通常会转到调整this指针的“thunk”。在您的示例中,casted1指针的 vtbl 条目不需要 thunk,因为它的IDerived1子对象CClass恰好与 CClass 对象的开头重合(这就是casted1指针值与指针相同的原因CClass object)。

但是,casted2指向IDerived2子对象的指针与对象的开头不一致CClass,因此 vtbl 函数指针实际上指向一个 thunk 而不是直接指向CClass::Common()函数。thunk 调整this指针以指向实际CClass对象,然后跳转到CClass::Common()函数。所以它总是会得到一个指向CClass对象开始的指针,不管它可能是从哪种类型的子对象指针调用的。

在 S tanley Lippman 的“Inside the C++ Object Model”一书中,第 4.2 节“Virtual Member Functions/Virtual Functions Under MI”对此有很好的解释。

于 2009-11-17T08:33:57.217 回答
1

If you see the object layout in memory for this it will be something like this:

v-pointer for IDerived1
v-pointer for IDerived2
....
....

It can be otherway also..but just to give an idea..

Your this will always point to the start of the object i.e. where the v-pointer for IDerived1 is stored. However, when you cast the pointer to IDetived2 the casted pointer will be pointing to the v-pointer for IDerived2 which will be offset by sizeof(pointer) from this pointer.

于 2009-11-17T07:57:17.540 回答
1

如您所示,G++ 4.3 中有相同的对象模型(请参阅 Naveen 的回答)说 casted1 和 casted2 具有不同的值。

此外,在 G++ 4.3 中,即使您使用残酷的强制转换:

ICommonBase* casted1 = (ICommonBase*)(IDerived1*)object; 
ICommonBase* casted2 = (ICommonBase*)(IDerived2*)object;

结果是一样的。

非常聪明的编译器

于 2009-11-17T08:25:31.260 回答