一个C++多继承带来的游戏开发陷阱(二)

2014-11-24 08:58:12 · 作者: · 浏览: 1
C++这门语言的特性之一,同时,也是复杂度的罪魁祸首之一!因为,在这里,编译器又背着我们做了一些事情,这也是C++饱受批评的主要原因。

请考虑下面的程序:

class A
{
public:
int m_a;
};

class B
{
public:
int m_b;
};

class C
{
public:
int m_c;
};

class D : public A, public B, public C
{
public:
int m_d;
};

A* a;
B* b;
C* c;
D d;
a = &d;
b = &d;
c = &d;

类关系如图所示:

这是一个最简单的MI体系,D继承自三个base class。我们再来看看它的内存模型:

可以看到,和SI不同的是,MI采用了非重叠模型——每个base class subobject都有自己的首地址。这里,A、B和C subobject各自占据它们自己的首地址,唯一的例外就是D object——也就是这个模型的拥有者,它的首地址和class A subobject是相同的。因此,我们说:

assert( a == &d );
assert( b != &d );
assert( c != &d );

“哎!等等!”,我听到了你在打断我,“我们在上面的程序中已经写明

b = &d;
c = &d;

这里为什么你会这么写:

assert( b != &d );
assert( c != &d );

你确定断言不会crash吗?”。如果你这么问我,我很高兴,这表明你在跟着我。下面是我通过试验得到的数据:

这就是问题的关键所在——编译器背着我们做了一件事情:this指针调整!在MI的世界里,this指针调整非常频繁,而这种调整,主要发生在 派生类对象 和“第二个以及后续的基类对象”(像咒语一样)之间的转换。在上面的例子里,“第二个以及后续的基类”就是类B和C。这个转换就是

b = &d; // upcast
c = &d; // upcast

this指针就是在这个时候被compiler调整的。b和c分别指向了正确的,属于它们各自的subobject的地址。同理,当我们将b和c转换成d指针的时候,this指针也会调整

D* d2 = static_cast(b); // downcast
D* d3 = static_cast(c); // downcast

结果是:

assert( d2 == &d );
assert( d3 == &d );

指针又被调整了回来。而这在SI的世界中是不会发生的(重叠模型)。

为什么要调整this指针呢?this指针调整的原因在于MI采用了非重叠的内存模型,而之所以采用这种模型,是为了保证各基类体系的完整性和独立性,保证virtual机制能够得以在MI的不同体系之间顺利运行(这通过每个subobject各自的vptr进行)。关于MI以及它的this指针调整,可以说的东西足够写成一本书(本文只是冰山一角),这里当然不行!关于MI的任何理论问题,你都可以在《Inside The C++ Object Model》一书中找到。

但是,如果你把上面我们讨论的理论都弄明白了,就足够理解下面的部分,以及一般的MI问题了。

问题分析

在掌握了SI和MI各自的基本知识之后,我们现在可以把之前的问题弹出堆栈!我们暂时离开实验室,来分析一下这个现实生活中的问题。Actor的继承体系如下所示:

老办法,我们分析一下它的内存模型:

该体系是一个SI和MI的混合体。可以把Actor看成是左右两个体系的MI类。Super hierarchy和EventedSprite这个SI作为第一个base class,ConcurrentJob和Path_Finder的这个SI看做是第二个base class。因此,

class Actor : public EventedSprite, public Path_Finder {...}

有关系:

Actor actor;
EventedSprite* spr = &actor; // 1
Path_Finder* path = &actor; // 2

assert( spr == &actor );
assert( path != &actor );

因为步骤2进行了this指针调整——这很清楚。好了,我们来看我们出问题的程序:

My_Task* myTask = new My_Task;
Actor* actor = new Actor;
myTask->setJob( actor );
System_Task_Manager_Or_Something->addToOperationQueue( myTask );

我们将actor交给了My_Task::setJob这个方法,该方法的形参是类型void*——它可以接受任何指针类型,这没有什么问题——我们只需要存储这个地址,在需要使用的时候用就是了。我们再看My_Task::run:

virtual void run()
{
ConcurrentJob* job = static_cast( m_job );
job->run();
}

m_job就是刚才被存储的Actor*——这个地址没问题。但,m_job的类型是void*——没有任何类型信息!我们应该对

ConcurrentJob* job = static_cast( m_job );

有什么期待呢?我们期待compiler会为我们调整this指针!因为ConcurrentJob是第二个基类体系,还记得 “第二个以及后续的基类”咒语吗?一个void*的指针,我们用编译期casting operator

static_cast

进行转换,是不会有任何地址上的变化的!Actor*的地址就这么直接赋予了ConcurrentJob*。this指针没有被调整!这个指针没有指向正确的subobject!导致了后面的严重错误!

一个快速的解决方案就是把m_job先变成Actor*,然后再转换。不过无论如何,只要能够给compiler足够的类型信息量,它就能做对事情——但前提是,你要先做对事情。