现在的位置: 首页 > 综合 > 正文

我的C++实践(18):多态的双重分派实现

2012年09月13日 ⁄ 综合 ⁄ 共 6985字 ⁄ 字号 评论关闭

    一般的多态是单重分派,即一个基类指针(或引用)直接到绑定到某一个子类对象上去,以获得多态行为。在前面“多态化的构造函数和非成员函数”介绍中,非成员函数函数operator<<实现了单重分派,它只有一个多态型的参数,即基类引用NLComponent&,通过在继承体系中定义一个统一的虚函数接口print来完成实际的功能,然后让operator<<的NLComponent&引用直接调用它即可,就可以自动地分派到某一个子类的print上去。
    但很多时候我们需要双重分派或多重分派。比如有一个外太空天体碰撞的视频游戏软件,涉及到宇宙飞船SapceShip、太空站SpaceStation、小行星Asteroid,它们都继承自GameObject。当天体碰撞时,需要调用processCollision(GameObject& obj1,GameObject& obj2)来进行碰撞处理,不同天体之间的碰撞产生不同的效果。这里有两个基类引用型的参数,它们的动态类型不同时需要做不同的碰撞处理,这就是双重分派。一种实现方案类似于前面的NLComponent,在各个天体类中定义统一的虚函数接口collide(GameObject&,GameObject&)来完成实际的碰撞处理,在processCollision中调用它即可。这样,在collide中我们要用一大堆的if/else来判断参数的动态类型(用typeid),根据不同的动态类型调用不同的碰撞处理函数,这种方法显然非常糟糕,它使得一个天体类需要知道它所有的兄弟类,特别地,如果增加一个新类(比如Satellite),那所有的类都需要修改collide,以增加对这个新类的判断,然后重新编译全部的代码。
    如果分析虚函数的实现机理,我们知道虚函数在编译器中通过虚函数表来实现,它是一个函数指针数组,数组的每个元素是一个函数指针,指向了实际要调用的虚函数,每个函数指针有一个唯一的下标索引,通过下标索引可以直接定位到该函数指针入口。这就启示我们,可以通过模拟虚函数表来实现双重分派。
    1、模拟虚函数表。我们把各个碰撞函数实现为非成员函数,参数的不同动态类型对应不同的碰撞函数。它们接受的参数都是两个GameObject&引用,这样所有的碰撞函数都具有相同的类型。定义一个map用来存放这种类型的函数指针,用函数参数的动态类型名称作为唯一的索引,由于有两个参数,因此把它们捆绑成一个pair对象来作为唯一的索引。这样,在processCollision中,直接根据两个参数的动态类型名称查找函数表,找到接受此参数的函数指针,然后调用这个碰撞函数进行处理即可。
    下面是天体类的继承体系:

    下面是碰撞处理的实现:

    解释:
    (1)各个碰撞处理函数的类型相同,都是void(GameObject&,GameObject&),因此在函数映射表中可以统一存放它们的指针。碰撞处理具有对称性,对称的版本直接交换一下参数来调用原来的版本即可。需要一个异常类,当没有找到对应的碰撞函数时,抛出异常。
    (2)把函数的两个参数的动态类型名称捆绑成pair对象,它的类型定义为StringPair,函数映射表的类型定义为HitMap。
    (3)主要有两个函数实现,在前面的匿名空间中进行了声明,然后在后面的匿名空间中进行了定义。一个初始化函数表initializeCollisionMap(),它创建实际的函数表,并把各个子类的名称和碰撞函数指针填入函数表中,返回函数表的指针。一个是查找碰撞函数指针的lookup(),它用静态的智能指针指向initializeCollisionMap()返回的函数表,表示创建唯一的一个函数。然后根据参数的动态类型名称查找函数表,找到则返回关联的碰撞函数指针。
    (4)这里使用了匿名的命名空间。匿名空间中所有的东西都局部于当前编译单元(本质上说就是当前文件),与其他文件中的同名实体无关系,它们的不同的实体。有了匿名命名空间,我们就无需使用文件作用域内的static变量(它也是局部于文件的),应该尽量使用匿名的命名空间。注意initializeCollisionMap()和lookup()在前面的匿名空间中声明了,因此后面的定义也必须放在匿名空间中,这样就保证了它们的声明和定义在同一编译单元内,链接器就能正确地将声明与本编译单元内的实现关联起来,而不会去关联别的编译单元内的同名实现。
    (5)全局的processCollision中,根据两个参数的动态类型名称查找函数表,找到接受此参数的函数指针,然后直接调用这个碰撞函数即可。
    (6)这里碰撞函数都是非成员函数。当增加新的GameObject子类时,原来的各个子类无需重新编译,也无需再维护一大堆的if/else。只需增加相应的碰撞函数,在initializeCollisionMap中增加相应的映射表项即可。
    2、函数表的改进。上面每增加一个碰撞函数时,都需要在initializeCollisionMap中静态地注册一个条目。我们可以把函数映射表的功能抽离出来,开发成一个独立的类CollisionMap,提供addEntry,removeEntry,lookup来动态地对函数表添加条目、删除条目、或者搜索指定的碰撞函数。我们还可以实现单例模式,让CollisionMap只能创建一个函数表。

    解释:
    (1)CollisionMap的实现是很直接的,它维护一个collisionMap表来模拟虚函数表。碰撞函数的添加、删除、搜索都比较容易。theCollisionMap返回唯一的一个函数映射表。
    (2)现在游戏开发者就不再需要initializeCollisionMap、lookup这样的函数了,直接用theCollisionMap()来动态地添加和删除碰撞函数,在processCollision直接用theCollisionMap()来搜索给定索引的碰撞函数即可。可见,这种模拟虚函数表的方法还可以推广到多重分派的情况。

抱歉!评论已关闭.