一般的多態是單重指派,即一個基類指標(或引用)直接到綁定到某一個子類對象上去,以獲得多態行為。在前面“多態化的建構函式和非成員函數”介紹中,非成員函數函數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中,直接根據兩個參數的動態類型名稱尋找函數表,找到接受此參數的函數指標,然後調用這個碰撞函數進行處理即可。
下面是天體類的繼承體系:
//GameObject.hpp:太空遊戲的架構<br />#ifndef GAME_OBJECT_HPP<br />#define GAME_OBJECT_HPP<br />class GameObject{ //表示天體的抽象基類<br />public:<br />//...<br />virtual ~GameObject()=0;<br />};<br />GameObject::~GameObject(){ //純虛的解構函式必須有定義<br />}<br />class SpaceShip : public GameObject{ //飛船類<br />public:<br />//...<br />};<br />class SpaceStation : public GameObject{ //空間站類<br />public:<br />//...<br />};</p><p>class Asteroid : public GameObject{ //小行星類<br />public:<br />//...<br />};</p><p>#endif
下面是碰撞處理的實現:
//collision.hpp:碰撞處理<br />#ifndef COLLISION_HPP<br />#define COLLISION_HPP<br />#include <string><br />#include <utility> //用到了pair及auto_ptr<br />#include <map><br />#include "GameObject.hpp"<br />namespace{<br />//主要的碰撞處理函數<br />void shipStation(GameObject& spaceShip,GameObject& spaceStation){<br />//處理SpaceShip-SpaceStation碰撞:比如讓雙方遭受與碰撞速度成正比的損壞<br />}<br />void shipAsteroid(GameObject& spaceShip,GameObject& asteroid){<br />//處理SpaceShip-Asteroid碰撞<br />}<br />void stationAsteroid(GameObject& spaceStation,GameObject& asteroid){<br />//處理SpaceStation-Asteroid碰撞<br />}<br />void shipShip(GameObject& spaceShip1,GameObject& spaceShip2){<br />//處理SpaceShip-SpaceShip碰撞<br />}<br />void stationStation(GameObject& spaceStation1,GameObject& spaceStation2){<br />//處理SpaceStation-SpaceStation碰撞<br />}<br />void asteroidAsteroid(GameObject& asteroid1,GameObject& asteroid2){<br />//處理Asteroid-Asteroid碰撞<br />} </p><p>//對稱的版本<br />void stationShip(GameObject& spaceStation,GameObject& spaceShip){<br />shipStation(spaceShip,spaceStation);<br />}<br />void asteroidShip(GameObject& asteroid,GameObject& spaceShip){<br />shipAsteroid(spaceShip,asteroid);<br />}<br />void asteroidStation(GameObject& asteroid,GameObject& spaceStation){<br />stationAsteroid(spaceStation,asteroid);<br />}</p><p>class UnknownCollision{ //不明天體碰撞時的異常類<br />public:<br />UnknownCollision(GameObject& object1,GameObject& object2){ }<br />};</p><p>typedef void (*HitFunctionPtr)(GameObject&,GameObject&); //指向碰撞函數的函數指標<br />typedef std::pair<std::string,std::string> StringPair; //關聯碰撞函數兩個參數的動態類型<br />//函數表的類型:每項關聯了碰撞函數兩個參數的動態類型名和碰撞函數本身<br />typedef std::map<StringPair, HitFunctionPtr> HitMap;</p><p>HitMap* initializeCollisionMap(); //初始化函數表<br />HitFunctionPtr lookup(std::string const& class1,<br />std::string const& class2); //在函數表中尋找需要的碰撞函數<br />} //end namespace<br />void processCollision(GameObject& object1,GameObject& object2){<br />////根據參數的動態類型尋找相應碰撞函數<br />HitFunctionPtr phf=lookup(typeid(object1).name(),typeid(object2).name());</p><p>if(phf)<br />phf(object1,object2); //調用找到的碰撞處理函數來進行碰撞處理<br />else<br />throw UnknownCollision(object1,object2); //沒有找到則拋出異常<br />}<br />namespace{<br />HitMap* initializeCollisionMap(){ //建立並初始化虛函數表<br />HitMap *phm=new HitMap; //建立函數表<br />//初始化函數表<br />(*phm)[StringPair(typeid(SpaceShip).name(),<br />typeid(SpaceStation).name())]=&shipStation;<br />(*phm)[StringPair(typeid(SpaceShip).name(),<br />typeid(Asteroid).name())]=&shipAsteroid;<br />(*phm)[StringPair(typeid(SpaceStation).name(),<br />typeid(Asteroid).name())]=&shipAsteroid;<br />//要包含所有的碰撞函數<br />//...<br />(*phm)[StringPair(typeid(Asteroid).name(),<br />typeid(SpaceStation).name())]=&asteroidStation;</p><p>return phm;<br />}<br />}<br />namespace{<br /> //根據參數類型名在函數表中尋找需要的碰撞函數<br /> HitFunctionPtr lookup(std::string const& class1,<br />std::string const& class2){<br />//用智能指標指向返回的函數表,為靜態,表示只能有一個函數表<br />static std::auto_ptr<HitMap> collisionMap(initializeCollisionMap());</p><p>HitMap::iterator mapEntry=collisionMap->find(make_pair(class1,class2));<br />if(mapEntry==collisionMap->end())<br />return 0; //沒找到,則返回null 指標<br />return (*mapEntry).second; //找到則返回關聯的碰撞函數<br />}<br />}</p><p>#endif
//GameTest.cpp:對遊戲架構的測試<br />#include <iostream><br />#include "GameObject.hpp"<br />#include "Collision.hpp"<br />int main(){<br />SpaceShip a;<br />SpaceStation b;<br />Asteroid c;<br />processCollision(a,b);<br />processCollision(a,c);<br />processCollision(b,c);<br />return 0;<br />}
解釋:
(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只能建立一個函數表。
//CollisionMap.hpp:碰撞處理函數的映射表,實現了單例模式<br />#ifndef COLLISION_MAP_HPP<br />#define COLLISION_MAP_HPP<br />#include <string><br />#include <utility> //用到了pair及auto_ptr<br />#include <map><br />#include "GameObject.hpp"<br />class CollisionMap{ //碰撞函數映射表<br />public:<br />typedef void (*HitFunctionPtr)(GameObject&,GameObject&); //指向碰撞函數的函數指標<br />typedef std::pair<std::string,std::string> StringPair; //關聯碰撞函數兩個參數的動態類型<br />typedef std::map<StringPair, HitFunctionPtr> HitMap;//函數表的類型</p><p>//根據參數類型名稱在函數映射表中尋找需要的碰撞函數<br />HitFunctionPtr lookup(std::string const& type1,<br />std::string const& type2){<br />HitMap::iterator mapEntry=collisionMap->find(make_pair(type1,type2));<br />if(mapEntry==collisionMap->end())<br />return 0; //沒找到,則返回null 指標<br />return (*mapEntry).second; //找到則返回關聯的碰撞函數<br />}<br />//根據參數類型名稱向映射表中加入一個碰撞函數<br />void addEntry(std::string const& type1,<br />std::string const& type2,<br />HitFunctionPtr collisionFunction){<br />if(lookup(type1,type2)==0) //映射表中沒找到時插入相應條目<br />collisionMap->insert(make_pair(make_pair(type1,type2),collisionFunction));<br />}<br />//根據參數類型名稱從映射表中刪除一個碰撞函數<br />void removeEntry(std::string const& type1,<br />std::string const& type2){<br />if(lookup(type1,type2)!=0) //若找到,則刪除該條目<br />collisionMap->erase(make_pair(type1,type2));<br />}<br />private:<br />std::auto_ptr<HitMap> collisionMap; //函數映射表,用智能指標儲存</p><p>//建構函式聲明為私人,以避免建立多個碰撞函數映射表<br />CollisionMap() : collisionMap(new HitMap){<br />}<br />CollisionMap(CollisionMap const&); //不會調用,無需定義<br />friend CollisionMap& theCollisionMap();<br />};<br />inline CollisionMap& theCollisionMap(){ //返回唯一的一個碰撞函數映射表<br />static CollisionMap co;<br />return co;<br />}<br />#endif
解釋:
(1)CollisionMap的實現是很直接的,它維護一個collisionMap表來類比虛函數表。碰撞函數的添加、刪除、搜尋都比較容易。theCollisionMap返回唯一的一個函數映射表。
(2)現在遊戲開發人員就不再需要initializeCollisionMap、lookup這樣的函數了,直接用theCollisionMap()來動態地添加和刪除碰撞函數,在processCollision直接用theCollisionMap()來搜尋給定索引的碰撞函數即可。可見,這種類比虛函數表的方法還可以推廣到多重指派的情況。