問題描述:
在 C++ 面向對象編程中,常常會遇到“類型識別”的需求:已知某個對象的基類型指針,需要識別該對象的派生類型,訪問派生類型的公有函數。簡單說就是,將基類型指針轉換為派生類型指針,并用來訪問派生類型的公有函數。
問題分析:
“類型識別”本質上是一個從抽象轉為具象的過程,破壞了對象抽象性。雖不值得提倡,但在具象化情景下,“類型識別”又是必須的。
“類型識別”常規方法是強制類型轉換。這需要先定義一個“對象類型”枚舉表,再在基類里定義一個返回“對象類型”的函數。這種方法的缺點顯而易見:
1. 需要維護全局性的“對象類型”枚舉表;
2. 向基類型構造函數傳入“對象類型”參數;
3. 每次“類型轉換”前,先要獲得“對象類型”值,判斷對象類型;
“類型識別”的另一種標準方法是RTTI。作為一項 C++ 編譯選項,RTTI 直接將上述常規方法教給編譯器做了。然而,這樣也是有缺點的:
1. RTTI會引入額外的開銷。這是可以理解的,問題是RTTI這種開銷會附加到所有類型上去;
2. RTTI要求所有庫都以RTTI方式編譯,才能正常工作。而現實世界總是復雜的,這一點未必總是能實現;
3. 個人認為RTTI是一種過于形式化的東西,違反了 C++ 的簡潔性、高效性原則;
4. 個人認為RTTI的低層形式化的東西,很多時候,我們仍然需要定義不依賴于C++類的“對象類型”枚舉表;
閱讀 WebKit 代碼時,我看到了一種基于虛函數的“類型識別”方法,代碼如下:
class EventTarget {
public:
void ref() { refEventTarget(); }
void deref() { derefEventTarget(); }
virtual EventSource* toEventSource();
virtual MessagePort* toMessagePort();
virtual Node* toNode();
virtual DOMWindow* toDOMWindow();
virtual XMLHttpRequest* toXMLHttpRequest();
virtual XMLHttpRequestUpload* toXMLHttpRequestUpload();
在基類中,定義向派生類型的轉換函數,并且返回為NULL。每個派生類型,重新實現向自身轉換的轉換函數,返回自身的指針。我感覺這種方法:
1. 非常安全,使用方便;
2. 效率相對比較高;
3. 省卻了全局性的“對象類型”枚舉表,卻帶來了維護基類“轉換函數”的負擔;
4. 造成了基類對派生類的依賴性,似乎不太好。
在上述方法的基礎上,設想了一種改進的方法,設計原則是“從基類型向派生類型轉換的函數,應該放置在派生類中,以靜態函數的方式定義”。具體方法如下:
1. 基類定義 void* toXXXX() 和 bool isXXXX() 兩個私有虛函數,用編程規范規定,禁止派生類直接訪問它們。
2. 派生類重定義上述函數。
3. 派生類型定義 XXXX* toType(Base*) 和 bool isType(Base*)兩個靜態函數,內部實現就是通過上述私有虛函數完成的。
4. 所有的“類型識別操作”都使用上述靜態函數完成。
問題小結:
上述四種方式,很難說哪一種方式更好,宜具體情況具體對待。
我本人偏向于,使用最后一種方式作為C++編程規范和模式,因為,它更是一種接口規范,其內部實現可以改為前三種方法中的任何一種。至于它帶來的些許工作量,我認為,在它成為一種工作規范后,那些少量代碼僅僅只是一點點沒有難度的機械性編程,完全可以忽略不計。如果不介意在代碼中使用宏機制,那么更可以用宏來實現,那么只需定義編譯宏就能就在三種方式之間任意切換。
最后一種方式,體現了一種策略:當你想為基類定義一些與派生類相關的工具函數時,倒不如將其定義為派生類的靜態函數,以避免派生類型對基類型的“污染”。