C++的RTTI 觀念和用途[轉]

C++的RTTI 觀念和用途 自從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)──支持執行時期的「往下變換型態」(downwar d 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-selectio n)方法﹐而不使用虛擬函數(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﹐則程序員毫無選擇必須使用虛擬函數來支持draw ing() 函數的多型性。於是程序員將draw()宣告爲虛擬函數﹐並寫drawing() 如下﹕ void drawing(Figure *p) { p->draw(); } 如此﹐Figure類別體系能隨時衍生類別﹐而不必修正drawing() 函數。亦即﹐Figur e體系有個穩定的接口(interface) ﹐drawing() 使用這接口﹐使得drawing() 函數也穩定 ﹐不會隨Figure類別體系的擴充而變動。這是封閉的一面。而這穩定的接口並未限制Figu re體系的成長﹐這是開放的一面。因而合乎「開放╱封閉」原則﹐軟件的結構會更具彈性 ﹐更易於隨環境而不斷成長。 RTTI的常見的 使用場合 一般而言﹐RTTI的常見使用場合有四﹕例外處理(exceptions handling)、動態轉型 態(dynamic casting) 、模塊整合、以及對象I/O 。 1.例外處理── 大家所熟悉的C++ 新功能﹕例外處理﹐其需要RTTI﹐如類別名稱等。 2.動態轉型態── 在類別體系(class hierarchy) 中﹐往下的型態轉換需要類別繼承的 RTTI。 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() ﹐將找不到data 值。若在執行時能利用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如──數據項的型態及位置(positio n)等皆未提供。 C++編譯器提供RTTI 由C++ 語言直接提供RTTI是最方便了﹐但是因RTTI的範圍隨應用場合而不同﹐若C++ 語 言提供所有的RTTI﹐將會大幅度增加C++ 的複雜度。目前﹐C++ 語言只提供簡單的RTTI﹐ 例如Borland C++ 新增typeid()操作數以及dynamic_cast函數樣版。請看個程序﹕ 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函數樣版── Figure *f; Square *s; f = new Rectangle(); s = dynamic_cast(f); if(!s) cout << "dynamic casting error!!" << endl; 在執行時﹐發現f 是不能轉爲Square *型態的。如下指令﹕ Figure *f; Rectangle *r; f = new Square(); r = dynamic_cast(f); if(r) r->Display(); 這種型態轉換是對的。 RTTI與虛擬函數表 在C++ 程序中﹐若類別含有虛擬函數﹐則該類別會有個虛擬函數表(Virtual Function T able﹐簡稱VFT )。爲了提供RTTI﹐C++ 就將在VFT 中附加個指針﹐指向typeinfo對象﹐ 這對象內含RTTI資料,如下圖: 由於該類別所誕生之各對象﹐皆含有個指針指向VFT 表﹐因之各對象皆可取出typeinf o對象而得到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++ Con ference, Portland, 1993. [注2] Meyer B.,Object-Oriented Software Construction, Prentice Hall, 1988.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章