Double Dispatch
如果同時有兩個多形的 reference 要執行某個動作,而且實際執行的動作,需要「同時」根據「兩個 reference 各自的實體」來決定,那這個動作稱為 double dispatch,因為需要根據兩個物件來分配執行的動作。上面的定義很抽象,書中舉了幾個例子,一個是在平面上,兩個 Shape 的重疊計算。Shape 有 Circle, Rectangle 等不同形狀的實作,如果要計算兩個 Shape 有沒有重疊,要先知道兩者確切的形狀,才能根據不同的形狀組合,用不同方式計算。
Shape 是二維的重疊,一維的重疊也有一樣的問題。像是電視預約錄影的時間表,有定時切台的 Reminder,有固定時段錄影的 RecordTimer,都繼承同一個 Event 結構。要檢查兩個 Event 有沒有重疊,也是把確切的 type 取出來,再針對三對不同 type 用不同方式計算重疊。
上面的兩個例子,兩個物件都是同一個多形繼承樹,而且運算有對稱。也可能是不同 class 的兩個物件,比如一筆抽象 Data 要給一個抽象 Device 來顯示,那就要根據具體 Data 和具體 Device 來決定實際的顯示方式。最後這個例子,可以很快的用兩組 virtual function 實作。既然是有兩個抽象物件,那就一次用一個 virtual 解出一個具體 type,經過兩次 virtual 就可以全部解出來。
class Device;
struct Data
{
virtual void output2(Device*) = 0;
virtual ~Data() {}
};
class Text;
class Image;
struct Device
{
virtual void display(Text*) = 0;
virtual void display(Image*) = 0;
virtual ~Device() {}
};
struct Text : public Data
{
virtual void output2(Device* dev)
{
dev->display(this);
}
};
struct Image : public Data
{
virtual void output2(Device* dev)
{
dev->display(this);
}
};
struct Screen : public Device
{
virtual void display(Text* text){} // Screen display Text
virtual void display(Image* image){} // Screen display Image
};
struct Printer : public Device
{
virtual void display(Text* text){} // Printer display Text
virtual void display(Image* image){} // Printer display Image
};
上面的例子有些特點,值得仔細思考。
- 假設兩個物件分別有 M, N 個實際的型別,最多就需要 M*N 這麼多個 function 來處理不同組合。另一個極端是,如果可以把界面全部都拉到抽象型別,只依賴抽象界面,那就不需要 double dispatch。
- 抽象型別本身有 cyclic dependency,是需要 class name。具體的實作,則需要知道另一邊的繼承樹上面的所有實作,才能針對每個繼承樹上面的 class 作不同的操作。也就是說 Screen includes Text and Image,才能實作 display()。
- 上面的例子,把 output2() 換成 accept(),display() 換成 visit(),就完全跟 visitor pattern 一樣了。可以說 visitor pattern 是個 double dispatch 特例,某些情況也可以直接拿 Ch10 Visitor template 來用,只是 template 固定用 accept/visit() 可能使得 function name 不合乎實際功能。
- 這些 display() 裡面的運算,很可能完全不需要 Screen/Printer private 資訊,應該分離這段算法,使用其他 public 界面來操作 Screen/Printer。但是因為要得到 Device& 指到的具體型別,需要用 virtual function 做 dispatch。
BasicFastDispatcher
首先,像上面的例子,利用 virtual function 可以解出一個 reference 指到的具體型別是什麼,比如每個 class 回傳一個獨特的 ID,之後就可以根據 ID 來判斷呼叫哪個 function。下面這個 macro 是個聰明的作法,加到每一個 class。一開始 ID 都是 -1,但是回傳 reference,這樣就可以在註冊每一組 class 要執行的 callback 的同時,指定正確的獨特 ID。等到需要 dispatch 的時候,呼叫 virtual GetClassIndex() 就可以知道是什麼 class。
#define IMPLEMENT_INDEXABLE_CLASS(SomeClass) \
static int& GetClassIndexStatic() \
{ \
static int index = -1; \
return index; \
} \
virtual int& GetClassIndex() \
{ \
assert(typeid(*this) == typeid(SomeClass)); \
return GetClassIndexStatic(); \
}
BasicDispatcher
除了加入 virtual function 以外,也可以使用 C++ typeid 運算子,來取得每個 class 獨特的 ID。這個作法跟上面的 BasicFastDispatcher 很像,註冊 callback 的時候用 typeid 紀錄每個 class 並且維護在 map 裡,執行時用 typeid 取得 reference 具體的 typeid,跟 map 比對就可以執行對應的 callback。這個方式比上面的 BasicFastDispatcher 慢的原因,應該是 typeid 只有大小順序,所以需要在 map 裡面搜尋。而 BasicFastDispatcher 使用的 ID 從 0 開始連續定義,可以直接用 vector 不需要搜尋。但是 BasicDispatcher 不需要改動原本的 class,通常是較好的選擇。
C++11: 舊標準 operator typeid return class type_info。C++11 提供 type_index 把 type_info 包起來,但仍然只能比大小。
StaticDispatcher
這是使用一連串 if-dynamic_cast 來偵測 reference 具體型別的作法,也就是把所有可能的 class 都 dynamic_cast 過,如果有 cast 出來,那就是找到了。缺點是 dynamic_cast 是很花時間的動作,但是這種作法需要執行 M+N 次 dynamic_cast。Summary
StaticDispatcher, BasicDispatcher, BasicFastDispatcher 書中有提供 template 實作,直接套用會比自己寫容易。如果是自己寫,常會在每個 object 塞一個 enum 用來區分 class,這就跟 BasicFastDispather 很接近。又或者一連串的 if-dynamic_cast,那就跟 StaticDispatcher 很像。這本書最後一章雖然很硬,但很實用。另外,這章只有討論到兩個多形物件的 double dispatch,三個或者更多個物件作法類似,也是要先想辦法得到具體 class。更常用的可能是 double dispatch 加上額外的參數,比如兩個 Shape 邊緣相切算不算重疊,可能是重疊計算額外的參數。如果要套用書上的 template,就需要增加參數。