C++中的執行中動態型別識別RTTI
RTTI綜述
C++中的2個運算子支援RTTI,即Run Time Type Identification:typeid和dynamic_cast。
RTTI實現的基石是每個型別對應的一個const type_info型別物件,它儲存了這個物件的確切型別資訊。注意,一個型別對應一個type_info物件,而不是一個物件。無論是基本型別還是使用者自定義型別,都需要額外的記憶體來存放此型別對應的type_info物件。一般情況,一個型別對應一個type_info物件。有的時候,需要為一種型別產生多個type_info物件:一個類繼承自多個繼承分支,並且多於或等於2個繼承分支上存在多型類。
type_info類過載了operator=()、operator!=(),另外一個常用的成員函式是name(),name成員函式返回字串表示的型別資訊。
typeid運算子
typeid()運算子和sizeof運算子一樣,是C++語言直接支援的。它以一個物件或者型別名作為引數,返回一個對應於該型別的const type_info物件(其實是這個型別對應的type_info物件的引用),表明該物件的確切型別。也可以使用typeid來檢視非多型型物件和基本資料型別物件的型別資訊。只不過,此時它不會去檢索物件的vptr和vtable,它們根本就沒有這些設施。此時typeid通過編譯器維護的資訊來返回結果,其結果仍然是運算元靜態型別對應的type_info物件。
在多型類的物件中,存在一個虛擬函式表指標vptr,指向型別對應的虛擬函式表vtable。vtable的第一項為一個type_info指標,指向該型別對應的type_info物件。vtable從第二項開始,就是該型別的虛擬函式指標了。
對多型類物件應用typeid操作符,需要檢索vptr指標,從vtable的第一項獲得該物件型別對應的type_info物件。這個過程和虛擬函式的動態繫結是相同的,它們的代價相同。
使用typeid的時候,需要注意:typeid()括號中的可以是引用,但使用指標的時候要解引用指標,如typeid(*p)。*p和p的type_info資訊是完全不同的。另外,對空指標進行typeid呼叫,會丟擲std::bad_typeid異常。
1: typedef unsigned int UINT;2: void f()3: {
4: cout << typeid(UINT).name() << endl; //"unsigned int"5: cout << typeid(string).name() << endl; //"string"6: }
7:
8: class HomeElectricDevice9: {
10: public:11: virtual void Open() = 0;12: virtual void Close() = 0;13: virtual void Adjust(bool updown) = 0;14: //Other virtual method15: private:16: //common attibutes17: };
18:
19: class ElectricFan:public HomeElectricDevice //電扇20: {
21: public:22: //redefine the virtual functions inherit from its base class23: private:24: //its own attributes25: };
26:
27: class Television:public HomeElectricDevice //TV28: {
29: public:30: //redefine the virtual functions inherit from its base class31: virtual void PlayVCD();32: private:33: //its own attributes34: };
35:
36: enum Command{OPEN, ClOSE, ADJUST, PLAY_VCD, /*others commands*/};37: class DeviceControllor38: {
39: public:40: Command GetCommand(){...};
41: void ControlThem(HomeElectricDevice&);42: };
43:
44: void DeviceControllor::ControlThem(HomeElectricDevice& device)45: {
46: Command cmd = GetCommand();
47: switch(cmd){48: case OPEN:49: device.Open();
50: break;51: case ClOSE:52: device.Close();
53: break;54: case ADJUST:55: device.Adjust(true);56: break;57: case PLAY_VCD: //不可以像OPEN那樣,直接通過device呼叫非基類的虛函數了58: //必須使用typeid了59: if (typeid(device) == typeid(Television)){ //只能識別Television物件,不能識別其子類60: Television *pTemp = static_cast<Television*>(&device);
61: pTemp->PlayVCD();
62: }else{63: cout << "This device cannot play VCD!" << endl;64: }
65: }
66: }
考慮一下程式碼中case PLAY_VCD的情況,如果以後再加入一個家庭影院FamilyCinema:: public Television會如何呢?那麼當傳入一個FamilyCinema的引用的時候,將提示This device cannot play VCD!。顯然不正確,我們如何擴充套件程式碼來解決這個問題呢?一個方法是加入一個if else分支,依舊用typeid來判斷。但是這個方法畢竟修改了程式碼。有沒有更好的辦法呢?
有,這就是dynamic_cast。
Dynamic_cast運算子
可以看出,typeid不具備可擴充套件性,因為它返回一個物件的確切型別資訊,不能將派生類匹配其父類。一個派生類如果是public繼承,那麼在語義上也應該可以看做基類物件。顯然typeid不具備這個能力。dynamic_cast同時具有執行時型別識別和轉換匹配2個功能。語法是:dynamic_cast<dest>(src);dest和src都必須為指標或者引用。其執行結果可以這樣描述:如果執行時src和dest所引用的物件,是相同型別,或者存在is-a關係(public繼承),則轉換成功。否則失敗。
dynamic_cast可以用來轉換指標和引用,不能轉化物件。它只能用來轉換多臺型別的物件指標或引用。
如果目標型別是指標時,成功則返回目標型別的指標,失敗返回NULL。如果目標型別是引用,成功則返回引用,失敗丟擲std::bad_cast異常,因為不存在NULL引用。
dynamic_cast的執行時型別識別功能的實現,和typeid一樣,通過指標或引用所繫結物件的虛擬函式表指標vptr,從vtable的第一項的type_info指標獲得物件型別對應的type_info物件。那麼,dynamic_cast的第二個功能“執行時型別的轉換匹配”如何實現的呢?為了實現這個功能,RTTI機制必須維護一棵繼承樹,dynamic_cast通過遍歷這個繼承樹來確定一個待轉換的物件和目標型別之間是否存在is-a關係。dynamic_cast的執行開銷顯然要比虛擬函式呼叫和typeid大,而且其開銷會隨著源物件型別與目標型別之間的層次的增大而增大。
使用dynamic_cast注意:轉換引用時,要有try、catch語句。轉換指標時,檢查是否轉換結果為NULL。
1: case PLAY_VCD:2: try{ //以後再新增Television的子類,也不需要修改程式碼3: Television& tv = dynamic_cast<Television&>(device); //只要device引用的是Television或其子類FamilyCinema即可4: tv.PlayVCD();
5: }catch(std::bad_cast&){6: cout << "This device cannot play VCD!" << endl;7: }
8: break;
總結
RTTI和虛擬函式動態繫結一樣,帶來好處的同時,也不是免費的午餐。它們帶來的執行速度和程式體積上的開銷。