C++中RTTI的觀念和使用 (2012-06-30 15:22)

C++中RTTI的觀念和使用 (2012-06-30 15:22)
標籤C++ RTTI 分類: C++


下面這篇文章雖然有點老,但對C++的RTTI基本原理講的比較透徹。
該文章摘自UMLCHINA網站,是臺灣一個羣體寫的,我根據大家比較熟悉的方式,修改了一些名詞的說法,如衍生(派生)等,讓大家可以方便的閱讀。
C++的 RTTI 觀念和用途
物澤C++應用小組
  自從1993年Bjarne Stroustrup 〔注1 〕提出有關C++ 的RTTI功能之建議﹐以及C++
的異常處理(exception handling)需要RTTI﹔最近新推出的C++ 或多或少已提供RTTI。然而,若不小心使用RTTI﹐可能會導致軟件彈性的降低。本文將介紹RTTI的觀念和近況﹐並說明如何善用它。
什麼是RTTI﹖
  在C++ 環境中﹐頭文件(header file) 含有類之定義(class definition)亦即包含有
關類的結構資料(representational information)。但是﹐這些資料只供編譯器(compi
ler)使用﹐編譯完畢後並未留下來﹐所以在執行時期(at run-time) ﹐無法得知對象的
類資料﹐包括類名稱、數據成員名稱與類型、函數名稱與類型等等。例如﹐兩個類﹐其繼承關係如下圖:
若有如下指令﹕
            Figure *p;
            p = new Circle();
           Figure &q = *p;
在執行時﹐p 指向一個對象﹐但欲得知此對象之類資料﹐就有困難了。同樣欲得知q 所參考(reference) 對象的類資料﹐也無法得到。
  RTTI(Run-Time Type Identification)就是要解決這困難﹐也就是在執行時﹐您想知
道指針所指到或參考到的對象類型時﹐該對象有能力來告訴您。
  隨着應用場合之不同﹐所需支持的RTTI範圍也不同。最單純的RTTI包括﹕
●類識別(class identification)──包括類名稱或ID。
●繼承關係(inheritance relationship)──支持執行時期的「往下變換類型」(downw
ard casting)﹐亦即動態變換類型(dynamic casting) 。
在對象數據庫存取上﹐還需要下述RTTI﹕
●對象結構(object layout) ──包括屬性的類型、名稱及其位置(position或offset
)。
●成員函數表(table of functions)──包括函數的類型、名稱、及其參數類型等。
其目的是協助對象的I/O 和持久化(persistence) ﹐也提供調試訊息等。
    若依照Bjarne Stroustrup 之建議〔注1 〕﹐C++ 還應包括更完整的RTTI﹕
●能得知類所實例化的各對象 。
●能參考到函數的源代碼。
●能取得類的有關在線說明(on-line documentation) 。
其實這些都是C++ 編譯完成時﹐所丟棄的資料﹐如今只是希望尋找個途徑來將之保留到執行期間。然而﹐要提供完整的RTTI﹐將會大幅提高C++ 的複雜度﹗
RTTI可能伴隨的副作用
  RTTI最主要的副作用是﹕程序員可能會利用RTTI來支持其「複選」(multiple-select
ion)方法﹐而不使用虛函數(virtual function)方法。
  雖然這兩種方法皆能達到多態化(polymorphism) ﹐但使用複選方法﹐常導致違反著名的「開放╱封閉原則」(open/closed principle) 〔注2 〕。反之﹐使用虛函數方法則可合乎這個原則,  請看下圖﹕
     Circle和Square皆是由Figure所派生出來的子類﹐它們各有自己的draw()函數。當
C++ 提供了RTTI﹐就可寫個函數如下﹕
 void drawing( Figure *p )
      {
           if( typeid(*p).name() == "Circle" )
                      ((Circle*)p)  ->  draw();
           if( typeid(*p).name() == "Rectangle" )
                      ((Rectangle*)p) -> draw();
       }
  雖然drawing() 函數也具有多型性﹐但它與Figure類體系的結構具有緊密的相關性。
當Figure類體系再派生出子類時﹐drawing() 函數的內容必須多加個if指令。因而違反
了「開放╱封閉原則」﹐如下﹕
很顯然地﹐drawing() 函數應加以修正。
      想一想﹐如果C++ 並未提供RTTI﹐則程序員毫無選擇必須使用虛函數來支持drawing() 函數的多型性。於是程序員將draw()宣告爲虛函數﹐並寫drawing() 如下﹕
     void drawing(Figure *p)
                 {     p->draw();      }
     如此﹐Figure類體系能隨時派生類﹐而不必修正drawing() 函數。亦即﹐Figure體
繫有個穩定的接口(interface) ﹐drawing() 使用這接口﹐使得drawing() 函數也穩定
﹐不會隨Figure類體系的擴充而變動。這是封閉的一面。而這穩定的接口並未限制Figure體系的成長﹐這是開放的一面。因而合乎「開放╱封閉」原則﹐軟件的結構會更具彈性﹐更易於隨環境而不斷成長。
RTTI的常見的     使用場合
      一般而言﹐RTTI的常見使用場合有四﹕異常處理(exceptions handling)、動態轉
類型(dynamic casting) 、模塊集成、以及對象I/O 。
1.異常處理──  大家所熟悉的C++ 新功能﹕異常處理﹐其需要RTTI﹐如類名稱等。
2.動態轉類型──  在類體系(class hierarchy) 中﹐往下的類型轉換需要類繼承的RT
TI。
3.模塊集成──  當某個程序模塊裏的對象欲跟另一程序模塊的對象溝通時﹐應如何得知對方的身分呢﹖知道其身分資料﹐才能呼叫其函數。一般的C++ 程序﹐常見的解決方法是──在源代碼中把對方對象之類定義(即存在頭文件裏)包含進來﹐在編譯時進行連結工作。然而﹐像目前流行的主從(Client-Server) 架構中﹐客戶端(client)的模塊對象﹐常需與主機端(server)的現成模塊對象溝通﹐它們必須在執行時溝通﹐但又常無法一再重新編譯。於是靠標頭文件來提供的類定義資料﹐無助於執行時的溝通工作﹐只得依賴RTTI了。
4.對象I/O ──  C++ 程序常將其對象存入數據庫﹐未來可再讀取之。對象常內含其它
小對象﹐因之在存入數據庫時﹐除了必須知道對象所屬的類名稱﹐也必須知道各內含小對象之所屬類﹐才能完整地將對象存進去。儲存時﹐也將這些RTTI資料連同對象內容一起存入數據庫中。未來﹐讀取對象時﹐可依據這些RTTI資料來分配內存空間給對象。
RTTI從那裏來﹖
  上述談到RTTI的用途﹐以及其副作用。這衆多爭論﹐使得RTTI的標準遲遲未呈現出來。也導致各C++ 開發環境提供者﹐依其環境所需而以各種方式來支持RTTI﹐且其支持RTTI的範圍也所不同。  目前常見的支持方式包括﹕
●由類庫提供RTTI──例如﹐Microsoft 公司的Visual C++環境。
●由C++ 編譯器(compiler)提供──例如﹐Borland C++ 4.5 版本。
●由源代碼產生器(code generator)提供──例如Bellvobr系統。
●由OO數據庫的特殊預處理器(preprocessor)提供──例如Poet系統。
●由程序員自己加上去。
這些方法皆只提供簡單的RTTI﹐其僅爲Stroustrup先生所建議RTTI內涵的部分集合而已。相信不久的將來﹐會由C++ 編譯器來提供ANSI標準的RTTI﹐但何時會訂出這標準呢﹖沒人曉得吧﹗
程序員自己提供的RTTI
  通常程序員自己可提供簡單的RTTI﹐例如提供類的名稱或識別(TypeID)。最常見的方法是﹕爲類體系定義些虛函數如Type_na() 及Isa() 函數等。請先看個例子﹕
 class Figure  { };
 class Rectangle : public Figure   { };
 class Square : public Rectangle
        {    int data;
            public:
              Square() { data=88; }
              void Display()  { cout << data << endl; }
          };
 void main()
       {   Figure *f = new Rectangle();
            Square *s = (Square *)f;
            s -> Display();
       }
這時s 指向Rectangle 之對象﹐而s->Display()呼叫Square::Display() ﹐將找不到da
ta值。若在執行時能利用RTTI來檢查之﹐就可發出錯誤訊息。於是﹐自行加入RTTI功能

  class Figure
      {  public:
             virtual char* Type_na()
                           {  return "Figure";  }
              virtual int Isa(char* cna)
                {  return !strcmp(cna, "Figure")? 1:0;  }
      };
 class Rectangle:public Figure
     {   public:
              virtual char* Type_na()
                     {  return "Rectangle";  }
              virtual int Isa(char* cna)
                 {  return !strcmp(cna, "Rectangle")?
                           1 : Figure::Isa(cna);
                 }
        static Rectangle* Dynamic_cast(Figure* fg)
                 {  return fg -> Isa(Type_na())?
                           (Rectangle*)fg : 0;
                 }
       };
  class Square:public Rectangle
          {    int data;
       public:
           Square() { data=88; }
            virtual char* Type_na()
                      {  return "Square";  }
            virtual int Isa(char* cna)
                {  return !strcmp(cna, "Rectangle")?
                           1 : Rectangle::Isa(cna);
                 }
        static Square* Dynamic_cast(Figure *fg)
                {  return fg->Isa(Type_na())?
                           (Square*)fg : 0;
                 }
         void Display()  {  cout << "888" << endl;  }
     };
虛函數Type_na() 提供類名稱之RTTI﹐而Isa() 則提供繼承之RTTI﹐用來支持「動態轉類型」函數──Dynamic_cast()。例如﹕
       Figure *f  =  new Rectangle();
       cout << f -> Isa("Square") << endl;
       cout << f -> Isa("Figure") << endl;
這些指令可顯示出﹕f 所指向之對象並非Square之對象﹐但是Figure之對象(含子孫對象)。再如﹕
       Figure *f;  Square *s;
       f  =  new Rectangle();
       s  =  Square  ==  Dynamic_cast(f);
       if(!s)
           cout << "dynamic_cast error!!" << endl;
此時﹐依RTTI來判斷出這轉類型是不對的。
類庫提供RTTI
  由類庫提供RTTI是最常見的﹐例如Visual C++的MFC 類庫內有個CRuntimeClass 類﹐其內含簡單的RTTI。請看個程序﹕
        class Figure:public CObject
           {
               DECLARE_DYNAMIC(Figure);
           };
    class Rectangle : public Figure
           {
              DECLARE_DYNAMIC(Rectangle);
           };
    class Square : public Rectangle
        {
           DECLARE_DYNAMIC(Square);
           int data;
         public:
           void Display()  {  cout << data << endl;  }
           Square()    {  data=88;  }
         };
    IMPLEMENT_DYNAMIC(Figure, CObject);
    IMPLEMENT_DYNAMIC(Rectangle, Figure);
    IMPLEMENT_DYNAMIC(Square, Rectangle);
Visual C++程序依賴這些宏(Macor) 來支持RTTI。現在就看看如何使用CRuntimeClass
類吧﹗如下﹕
          CRuntimeClass *r;
          Figure *f  =  new Rectangle();
          r = f -> GetRuntimeClass();
          cout << r -> m_psClassName << endl;
     這就在執行時期得到類的名稱。Visual C++的類庫僅提供些較簡單的RTTI──類名稱、對象大小及父類等。至於其它常用的RTTI如──數據項的類型及位置(position)等皆未提供。
C++編譯器提供RTTI
  由C++ 語言直接提供RTTI是最方便了﹐但是因RTTI的範圍隨應用場合而不同﹐若C++語言提供所有的RTTI﹐將會大幅度增加C++ 的複雜度。目前﹐C++ 語言只提供簡單的RTTI﹐例如Borland C++ 新增typeid()操作數以及dynamic_cast<T*>函數樣版。請看個程序﹕
   class Figure
        {   public:
                virtual void Display();
         };
   class Rectangle : public Figure   { };
   class Square:public Rectangle
      {    int data;
          public:
            Square() {  data=88;  }
             void Display() {  cout << data << endl;  }
       };
現在看看如何使用typeid()操作數──
          Figure *f  =  new Square();
          const typeinfo  ty  =  typeid(*f);
          cout << ty.name() << endl;
這會告訴您﹕f 指針所指的對象﹐其類名稱是Square。再看看如何使用dynamic_cast<T*>函數樣版──
      Figure *f;  Square *s;
      f = new Rectangle();
      s = dynamic_cast<Sqiare *>(f);
      if(!s)
          cout << "dynamic casting error!!" << endl;
在執行時﹐發現f 是不能轉爲Square *類型的。如下指令﹕
       Figure *f;  Rectangle *r;
       f = new Square();
       r = dynamic_cast<Rectangle *>(f);
       if(r)    r->Display();
這種類型轉換是對的。
RTTI與虛函數表
在C++ 程序中﹐若類含有虛函數﹐則該類會有個虛函數表(Virtual Function Table﹐
簡稱VFT )。爲了提供RTTI﹐C++ 就將在VFT 中附加個指針﹐指向typeinfo對象﹐這對象內含RTTI資料,如下圖:
   由於該類所實例化之各對象﹐皆含有個指針指向VFT 表﹐因之各對象皆可取出typeinfo對象而得到RTTI。例如﹐
          Figure *f1 = new Square();
          Figure *f2 = new Square();
          const typeinfo ty = typeid(*f2);
其中﹐typeid(*f2) 的動作是﹕
1.取得f2所指之對象。
2.從對象取出指向VMF 之指針﹐經由此指針取得VFT 表。
3.從表中找出指向typeinfo對象之指針﹐經由此指針取得typeinfo對象。
  這typeinfo對象就含有RTTI了。參考下圖1,經由f1及f2兩指針皆可取得typeinfo對象﹐所以   typeid(*f2) == typeid(*f1)。
總結
  RTTI是C++ 的新功能。過去﹐C++ 語言來提供RTTI時﹐大多依賴類庫來支持﹐但各類庫使用的方法有所不同﹐使得程序的可移植性(portability) 大受影響。然而﹐目前C++ 也只提供最簡單的RTTI而已﹐可預見的未來﹐當大家對RTTI的意見漸趨一致時﹐C++ 將會提供更完整的RTTI﹐包括數據項和成員函數的類型、位置(offset)等資料﹐使得C++ 程序更井然有序﹐易於維護。
參考資料
[注1]  Stroustrup B., “Run-Time Type Identification for C++”, Usenix C++ C
onference, Portland, 1993.
[注2] Meyer B.,Object-Oriented Software Construction, Prentice Hall, 1988
(function(w, d, g, J) { var e = J.stringify || J.encode; d[g] = d[g] || {}; d[g]['showValidImages'] = d[g]['showValidImages'] || function() { w.postMessage(e({'msg': {'g': g, 'm':'s'}}), location.href); } })(window, document, '__huaban', JSON);

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章