運行時類型識別(RTTI)的引入有三個做用:html
1.1. 靜態類型的情形linux
C++中支持使用typeid關鍵字獲取對象類型信息,它的返回值類型是const std::type_info&,例:c++
#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返回的就是該對象的引用。git
經過查看clang編譯器生成的LLVM彙編程序(LLVM彙編程序比本地彙編程序可讀性較強),能夠證實這一點。
使用clang編譯上述源碼:「clang -S -emit-llvm test.cpp -o -」,生成LLVM彙編程序包含如下信息(爲了方便閱讀,省略了部分無關內容):github
@_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 ...
其中:編程
附加說明:svn
1.2. 動態類型的情形函數
當typeid的操做數引用的是一個動態類(含有虛函數的類) 類型時,它的返回值是被引用對象對應類型的類型信息對象,例:spa
#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能夠直接返回這裏的類型信息對象引用便可。
下面的圖示描述了這一過程:
catch的匹配過程也可利用與typeid類似的原理進行類型匹配判斷,此再也不贅述。
說明:本節不考慮虛擬繼承的情形。
先上一個例子:
轉換過程:
(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對象指針,成樹狀結構:
所以繼承關係能夠經過此樹狀結構判斷,有了繼承關係,再遞歸從虛表中查找基類子對象在派生類中的偏移值,即可以肯定最終返回地址。
(1) Itanium C++ ABI
(2) LLVM Language Reference Manual
(3) libc++abi源碼(private_typeinfo.h文件)