1. 程式人生 > >C++雜記:運行時類型識別(RTTI)與動態類型轉換原理

C++雜記:運行時類型識別(RTTI)與動態類型轉換原理

程序包 bar ons Language 值類型 包括 iat www !=

運行時類型識別(RTTI)的引入有三個作用:

  1. 配合typeid操作符的實現;
  2. 實現異常處理中catch的匹配過程;
  3. 實現動態類型轉換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)與動態類型轉換原理