C++雜記:運行時類型識別(RTTI)與動態類型轉換原理
運行時類型識別(RTTI)的引入有三個作用:
- 配合typeid操作符的實現;
- 實現異常處理中catch的匹配過程;
- 實現動態類型轉換dynamic_cast。
1. typeid操作符的實現
1.1. 靜態類型的情形
C++中支持使用typeid關鍵字獲取對象類型信息,它的返回值類型是const std::type_info&,例:
#include <typeinfo> #include <cassert> struct B {} b, c; struct D : B {} d; void test() { const std::type_info& tb = typeid(b); const std::type_info& tc = typeid(c); const std::type_info& td = typeid(d); assert(tb == tc); // b和c具有相同的類型 assert(&tb == &tc); // tb和tc引用的是相同的對象 assert(tb != td); // 雖然D是B的子類,但是b和d的類型卻不同 assert(&tb != &td); // tb和td引用的是不同的對象 }
理論上講,編譯器會為每一種類型生成一個能唯一標識該類型的類型信息對象,typeid返回的就是該對象的引用。
通過查看clang編譯器生成的LLVM匯編程序(LLVM匯編程序比本地匯編程序可讀性較強),可以證明這一點。
使用clang編譯上述源碼:“clang -S -emit-llvm test.cpp -o -”,生成LLVM匯編程序包含以下信息(為了方便閱讀,省略了部分無關內容):
@_ZTI1B = linkonce_odr constant { i8*, i8* } { ... } @_ZTI1D = linkonce_odr constant { i8*, i8*, i8* } { ... } define void @_Z4testv() #0 { %tb = alloca %"class.std::type_info"*, align 8 %tc = alloca %"class.std::type_info"*, align 8 %td = alloca %"class.std::type_info"*, align 8 store bitcast ({ i8*, i8* }* @_ZTI1B to %"class.std::type_info"*), %tb, align 8 store bitcast ({ i8*, i8* }* @_ZTI1B to %"class.std::type_info"*), %tc, align 8 store bitcast ({ i8*, i8*, i8* }* @_ZTI1D to %"class.std::type_info"*), %td, align 8 ...
其中:
- @_ZTI1B 和@_ZTI1D 是兩個全局變量,用以存儲std::type_info(或者其子類)對象。
- 上述LLVM匯編程序中還列出了test()函數的起始部分內容,其中將@_ZTI1B 存儲於%tb和%tc,將@_ZTI1D 存儲於%td,正好對應原程序中的引用初始化語句。
附加說明:
- LLVM匯編語言也稱之為LLVM中間表示(IR, Intermediate Representation),其中全局變量以“@”開頭。詳細請參見:LLVM Language Reference Manual。
- _ZTI1B和_ZTI1D是經過名字修飾(name mangling)修飾之後的變量名,linux下可以使用c++filt命令還原成可讀形式(例如:c++filt _ZTI1B輸出“typeinfo for B”,說明_ZTI1B是標識B類型的全局變量)。
1.2. 動態類型的情形
當typeid的操作數引用的是一個動態類(含有虛函數的類) 類型時,它的返回值是被引用對象對應類型的類型信息對象,例:
#include <typeinfo> #include <cassert> struct B { virtual void foo() {} }; struct C { virtual void bar() {} }; struct D : B, C {}; void test() { D d; B& rb = d; C& rc = d; assert(typeid(rb) == typeid(d)); // rb引用的類型與d相同 assert(typeid(rb) == typeid(rc)); // rb引用的類型與rc引用的類型相同 }
編譯時可能還不知道rb或rc引用的類型,運行時怎麽能判斷該返回基類還是派生類對應的類型信息對象呢?
還記得“C/C++雜記:深入虛表結構”一文中講過的-fdump-class-hierarchy選項吧,用它將D的虛表打印出來如下:
可見,無論是“主虛表”還是“次虛表”,其中的RTTI信息位置都是&_ZTI1D(即D類型對應的類型信息對象)。
正是利用了這一點,運行時便可以通過vptr找到“虛函數表”,而“虛函數表”之前的一個位置存放了需要的類型信息對象,typeid可以直接返回這裏的類型信息對象引用即可。
下面的圖示描述了這一過程:
2. 實現異常處理中catch的匹配過程
catch的匹配過程也可利用與typeid相似的原理進行類型匹配判斷,此不再贅述。
3. 動態類型轉換(dynamic_cast)
說明:本節不考慮虛擬繼承的情形。
先上一個例子:
轉換過程:
(1) 對#2來說最為簡單,首先獲取RTTI對象,RTTI對象與目標類型信息對象一致,而偏移值也為0,所以只用返回源地址(pb)即可。
(2) 對#1和#3來說,RTTI對象與目標類型信息對象一致,但是有偏移值-8,所以返回值為“(char*)pa + (-8)”或“(char*)pc + (-8)”。
(3) 對#4來說,RTTI對象與目標類型信息對象不一致,但是目標類型C 是RTTI對象表示類型(D)是基類(後面會討論如何判斷繼承關系),因此轉換也是可行的。
用clang編譯上述源碼,生成LLVM匯編程序如下(已作簡化):
@_ZTI1A= linkonce_odr constant { i8*, i8* } { ... } @_ZTI1B= linkonce_odr constant { i8*, i8* } { ... } @_ZTI1C= linkonce_odr constant { i8*, i8*, i8* } {..., i8* bitcast ({ i8*, i8* }* @_ZTI1A to i8*) } @_ZTI1D= linkonce_odr constant { i8*, i8*, i32, i32, i8*, i64, i8*, i64 } { ..., i8* bitcast ({ i8*, i8* }* @_ZTI1B to i8*), i64 2, i8* bitcast ({ i8*, i8*, i8* }* @_ZTI1C to i8*), i64 2050 }
從中可以看出,RTTI對象中存放的內容還包括基類的RTTI對象指針,成樹狀結構:
因此繼承關系可以通過此樹狀結構判斷,有了繼承關系,再遞歸從虛表中查找基類子對象在派生類中的偏移值,便可以確定最終返回地址。
4. 參考
(1) Itanium C++ ABI
(2) LLVM Language Reference Manual
(3) libc++abi源碼(private_typeinfo.h文件)
http://www.cnblogs.com/malecrab/p/5574070.html
C++雜記:運行時類型識別(RTTI)與動態類型轉換原理