我們中國人崇拜龍,所謂“龍生九種,九種各別”。哪九種?《西游記》裡西海龍王對孫悟空說:“第一個小黃龍,見居淮渎;第二個小骊龍,見住濟渎;第三個青背龍,占了江渎;第四個赤髯龍,鎮守河渎;第五個徒勞龍,與佛祖司鐘;第六個穩獸龍,與神官鎮脊;第七個敬仲龍,與玉帝守擎天華表;第八個蜃龍,在大家兄處砥據太岳。此乃第九個鼍龍,因年幼無甚執事,自舊年才著他居黑水河養性,待成名,別遷調用,誰知他不遵吾旨,沖撞大聖也。”(注:鼍龍是文雅的說法,民間叫法是豬婆龍,也就是揚子鳄。)如果您沖著這九位說一聲“Let’s go”,那場面可壯觀了,有天上飛的,有水裡游的,也有地上爬的。同樣是“go”,“go”的具體形式卻各不相同,這正是多態“一個接口,多種實現”的典型例子。
多態的實現方法很多,其中C++直接支持的方式有:通過關鍵字virtual提供虛函數進行遲後聯編,以及通過模板(template)實現靜態多態性,它們都各有用武之地。我們比較熟悉的是虛函數,這是建構類層次的重要手段,我們也已經分析過虛函數的原理[1]。然而在有些情況下,虛函數的性能並不是最優,故VCL還提供了一種動態(dynamic)函數,用法和虛函數一模一樣,只要把virtual換成DYNAMIC就可以了。VCL的幫助文件裡說,動態函數跟虛擬函數相比,空間效率占優,時間效率不行,真的嗎?其實現原理又是如何呢?我們又應該如何權衡這兩者的使用呢?我們將從一個相當一般的角度來討論這些問題。
如下類層次來自一個圖形繪制程序的一部分。為了方便管理,界面與具體的圖形設計分離。各種圖形以動態連接庫的方式提供,作為插件的形式。這樣可以在不重新編譯主程序的情況,增加或減少各種圖形。
圖1 Shape類層次
最初Shape的聲明是
class Shape { private: int x0, y0; protected: Shape(); virtual ~Shape(); public: int x() const; int y() const; virtual void draw(void *) = 0; virtual int move(int, int); };後來因為功能擴充,添加了兩個虛函數。
class Shape { private: int x0, y0; protected: Shape(); virtual ~Shape(); public: int x() const; int y() const; virtual int move(int, int); virtual void draw(void *) = 0; virtual void save(void *) const = 0; virtual void load(void *) = 0; };後來又作過一些修改,又添加了若干虛函數。問題就在於,虛函數一但增加,虛擬函數表VFT就會發生變化,這時候,主程序就必須重新編譯。更糟糕的是,一旦版本升級,派生自不同版本Shape的圖形絕對不可以混用[2]。所以我們可以看到硬盤裡充斥著mfc20.dll、mfc40.dll、mfc42.dll……卻一個也不能刪除,這就是MFC升級所帶來的DLL垃圾。怎麼辦?
我在網上問過這樣的問題,得到的答復主要有:
其實上面的方法都能很好地解決這個問題。但是推廣看來,也有一定局限性。COM不適合解決類層次過深的情況,預留的空間則是不折不扣的“雞肋”。
追根究底,這個局限性是因為父類和子類的虛擬函數表VFT之間過強的關聯性:子類的VFT的前面一部分必須與父類相同。而當父類和子類不在同一個DLL或EXE中的時候,這個要求是很難滿足的。父類一旦改變,子類如果不重新編譯,就將導致錯誤。解決的方法,當然就是取消父類和子類VFT之間的關聯性。我設計了一個很笨的解決辦法,但可以取消這個關聯性,使虛函數保證始終只有2個。
#define Dynamic // Dynamic什麼都不是,只是好看一點 struct point { int x, y; }; class dispatch_error{}; class Shape { private: int x0, y0; protected: Shape(); virtual ~Shape(); virtual void dispatch(int id, void* in, void* out); // in和out是函數的輸入輸出參數,id是每個函數唯一的標記符號,即代號 // 實際運用中,id不一定是整數,也可以是128位UUID,或者字符串等等 public: int x() const; int y() const; Dynamic int move(int dx, int dy) { int r; point p = {dx, dy}; dispatch(-1, &p, &r); return r; } Dynamic void draw(void *hdc) {dispatch(-2, hdc, 0);} Dynamic void save(void* o) const {dispatch(-3, o, 0);} Dynamic void load(void* i) {dispatch(-4, i, 0);} }; void Shape::dispatch(int id, void* in, void* out) { switch(id) { case -1: ... case -2: ... ... default: throw(dispatch_error()); // 若函數不存在則拋出異常 } }如果子類Triangle要改寫Shape::draw,那麼只需要
void Triangle::dispatch(int id, void* in, void* out) { switch(id) { ... case -2: // 改寫Shape::draw ... ... default: Shape::dispatch(id, in, out); //函數不存在則向父類找 } }這樣的“Dynamic函數”就解決了前面的問題,只有析構和dispatch這兩個虛函數。父類和子類的VFT之間沒有關聯性,可以自由改動而不會互相影響。
我們來對這種解決方案作了評價:的確解決了虛函數的問題,但是也付出了不小的代價:時間效率和可讀性,由此也決定了該方案的應用面不廣,一般用於
從模式(Patterns)的角度來看,這種方法是典型的職責鏈(Chain of Responsibility)模式[4]:調用請求從最低層子類開始一層層往上傳遞,直到被處理或者最後拋出異常。這種模式運用非常廣泛,比如VCL消息映射[5]和COM中IDispatch接口[6],與上述解決方案的形式都非常相似。
這個解決方案還可以作進一步的完善,以更好地適用於單根結構的框架。比如單根結構的類庫,如MFC和VCL,通過RTTI可以找到唯一的父類,那麼可以分離數據(函數代號和指針)和代碼(調配部分),以簡化結構。解決的方法就是典型的表格驅動,有不少書[7,8]都用此來優化COM中IUnkown接口的QueryInterface。我們引入類DMT來儲存函數的代號和指針。
#includeusing namespace std; class DMT { char* const ptr; const DMT* const parent; public: DMT(const DMT* const, const int, ...); ~DMT() {delete []ptr;} short size() const {return *(short*)ptr;} const void* find(int) const; };
圖2 類DMT圖解
需要特別注意的是DMT::ptr所分配的空間。在32位系統上,對於n個“Dynamic函數”,需要sizeof(short)字節儲存n(紅色部分),sizeof(void*)*n字節儲存函數代號(黃色部分),以及sizeof(void*)*n字節儲存函數指針(藍色部分),一共是sizeof(short) + 2*n*sizeof(void*)字節。子類和父類的DMT可以通過鏈表形式連接起來。下面我們看看DMT::find和DMT::DMT的實現。
const void* DMT::find(int i) const { const int* begin = (int*)(ptr + sizeof(short)), *p; for(p = begin; p < begin + size(); ++p) if(*(int*)p == i) return *(void**)(p+ size()); // 找到對應的函數代號後,向前跳DMT::size()則是相應的函數指針 return (parent)? parent->find(i): 0; } DMT::DMT(const DMT* const p, const int n, ...) : parent(p), ptr(new char[sizeof(short)+2*n*sizeof(void*)]) // ptr分配的空間大小如前所述 { int* i = (int*)(ptr + 2), c; *(short*)ptr = n; // 往頭sizeof(short)字節寫入n(紅色部分) va_list ap; va_start(ap, n); for(c = 0; c < n; ++c) // 往黃色部分寫入函數代號 *(i++) = va_arg(ap, int); typedef void (DMT::*temp_type)(); temp_type temp; for(c = 0; c < n; ++c) // 往藍色部分寫入函數指針 { temp = va_arg(ap, temp_type); *(i++) = *(int*)&temp; } va_end(ap); }下面我們在Shape類層次中應用DMT類。
class Shape { private: