站在對象模型的尖端(On the Cusp of the Object Model)
Template
下面是有關template的三個主要討論方向:
- template的聲明,基本上來說就是當你聲明一個template class、template class member function等等,會發生什么事情。
- 如何"具現(instantiates)"出class object以及inline nonmember,以及member template functions,這些是"每一個編譯單元都會擁有的一份實體"的東西。
- 如何“具現”出nonmember以及member templates functions,以及static template class members,這些都是"每一個可執行文件中只需要一份實體"的東西,這也就是一般而言template所帶來的問題。
Template的"具現"行為(Template Instantiation)
考慮下面的template Point class:
template<class Type>
class Point{
public:
enum Status { unallocated, normalized };
Point(Type x = 0.0, Type y = 0.0, Type z = 0.0);
~Point();
void *operator new(size_t );
void operator delete(void *, size_t );
//...
private:
static Point<Type> *freeList;
static int chunkSize;
Type _x, _y, _z;
};
首先,當編譯器看到template class聲明時,它會做出什么反應?在實際程序中,什么反應也沒有!也就是說,上述的static data members並不可用。nested enum或其enumerators也一樣。
雖然enum Status的真正類型在所有的Point instantiations中都一樣,其enumerators也是,但它們每一個都只能通過template Point class的某個實體來存取或操作,因此我們可以這樣寫:
Point<float>::Status s;
但是不能這樣寫:
//error
Point::Status s;
同樣的道理,freeList和chunkSize對程序而言也還不可用,我們不能夠寫:
//error
Point::freeList;
我們必須明確地指定類型,才能使用freeList:
Point<float>::freeList;
像上面這樣使用static member,會使其一份實體與Point class的float instantiation在程序中產生關聯,如果我們寫:
//ok, 另一個實體(instance)
Point<double>::freeList;
就會出現第二個freeList實體,與Point class的double instantiation產生關聯
一個class object的定義,不論是由編譯器暗中地做,或是由程序員像下面這樣明確地做:
const Point<float> origin;
都會導致template class的“具現”,也就是說,float instantiation的真正對象布局會被產生出來。
member functions(至少對於那些未被使用過的)不應該被“實體”化,只有在member functions被使用的時候,C++ Standard才要求它們被“具現”出來。當前的編譯器並不精確遵循這項要求,之所以由使用者來主導“具現”規則,有兩個主要原因:
- 空間和時間效率的考慮。如果class中有100個member functions,但你的程序只針對某個類型使用其中兩個,針對另一個類型使用其中5個,那么其他193個函數都“具現”將花費大量的時間和空間。
- 尚未實現的功能,並不是一個template具現出來的所有類型就一定能夠支持一組member functions所需要的所有運算符。如果只“具現”那些真正用到的memeber functions,template就能夠支持那些原本可能會造成編譯時期錯誤的類型(types)。
Template中的名稱決議方式(Name Resolution within a Template)
你必須能夠區分以下兩種意義。一種是C++ Standard所謂的"Scope of the template definition",也就是“定義出template”的程序。另一種是C++ Standard所謂的"scope of the template instantiation",也就是說“具現出template”的程序。第一種情況舉例如下:
//scope of the template definition
extern double foo(double);
template<class type>
class ScopeRules{
public:
void invariant(){
_member = foo(val);
}
type type_dependent(){
return foo(_member);
}
//...
private:
int _val;
type _member;
};
第二種情況舉例如下:
//scope of the template instantiation
extern int foo(int);
//...
ScopeRultes<int> sr0;
在ScopeRules template中有兩個foo()調用操作。在“scope of template definition”中,只有一個foo()函數聲明位於scope之內。然而在“scope of template instantiation”中,兩個foo()函數聲明都位於scope之內。如果我們有一個函數調用操作:
//scope of the template instantiation
sr0.invariant();
那么,在invariant()中調用的究竟是哪一個foo()函數實體呢?
//調用的是哪一個foo()函數實體
_member = foo(_val);
在調用操作的那一點上,程序中的兩個函數實體是:
//scope of the template declaration
extern double foo(double);
//scope of the template instantiation
extern int foo(int);
而_val的類型是int,那么你認為選中的是哪一個呢?結果,被選中的是直覺以外的那一個:
//scope of the template declaration
extern double foo(double);
Template之中,對於一個nonmember name的決議結果是根據這個name的使用是否與“用以具現出該template的參數類型”有關而設定的。如果其使用互不相關,那么就以“scope of the template declaration”來決定name。如果其使用互有關聯,那么就以“scope of template instantiation”來決定name。在第一個例子中,foo()與用以具現ScopeRules的參數類型無關:
//the resolution of foo() is not
//dependent on the template argument
_member = foo(val);
這是因為_val的類型是int, _val是一個“類型不會變動”的template class member。也就是說,被用來具現出這個template的真正類型,對於 _val的類型並沒有影響。此外,函數的決議結果只和函數的原型(signature)有關,和函數的返回值沒有關聯。因此, _
member的類型並不會影響哪一個foo()實體被選中。foo()的調用與template參數毫無關聯! 所以調用操作必須根據"scope of the template declaration"來決議。在此scope中,只有一個foo()候選者。
讓我們另外看看"與類型相關"(type-dependent)的用法:
sr0.type_dependent();
這個函數的內容如下:
return foo(_member);
它究竟會調用哪一個foo()呢?
這個例子很清楚地與template參數有關,因為該參數將決定_member得真正類型。所以,這一次foo()必須在"scope of the template instantiation"中決議,本例中這個scope有兩個foo()函數聲明。由於 _member的類型在本例中為int,所以應該是int版的foo()出線。如果ScopeRules是以unsigned int或long類型具現出來,那么foo()調用操作就曖昧不明。最后,如果ScopeRules是以某一個class類型具現出來,而該class沒有針對int或double實現出convertion運算符,那么foo()調用操作會被標識為錯誤。不管如何改變,都是由"scope of the template instantiation"來決定,而不是由"scope of the template declaration"決定。
這意味着一個編譯器必須保持兩個scope contexts:
- “Scope of the template declaration”,用以專注於一般的template class
- "Scope of the template instantiation", 用以專注於特定的實體
編譯器的決議(resolution)算法必須決定哪一個才是適當的scope,然后在其中搜尋適當的name。
Member Function的實例化行為(Member function instantiation)
對於template的支持,最困難莫過於template function的具現(instantiation),目前的編譯器提供了兩個策略:一個是編譯時期策略,程序代碼必須在program text file中備妥可用;另一個是鏈接時期策略,程序代碼必須在meta-compliation工具可以導引編譯器的具現行為(instantiation)。
下面是編譯器設計者必須回答的三個主要問題:
- 編譯器如何找出函數的定義?
答案之一是包含template program text file,就好像它是個header文件一樣,Borland編譯器就是遵循這個策略。另一種方法是要求一個文件命名規則,例如,我們可以要求,在Point.h文件中發現的函數聲明,其template program text一定要放置於文件Point.c或者Point.cpp中,以此類推。cfront就是遵循這個策略。Edison Desigin Group編譯器對此兩種策略都支持。 - 編譯器如何能夠只具現出程序中用到的member functions?
解決辦法之一就是,根本忽略這項要求,把一個已經具現出來的class的所有member functions都產生出來。Borland就是這么做的——雖然它也提供#pragmas讓你壓制(或具現出)特定實體。另一種策略就是仿真鏈接操作,檢測看看哪一個函數真正需要,然后只為它(們)產生實體。cfront就是這么做的,Edison Design Group編譯器對此兩種策略都支持。 - 編譯器如何阻止member definitions在多個 .o文件中都被具現呢?
解決辦法之一是產生多個實體,然后從鏈接器中提供支持,只留下其中一個實體,其余都忽略。另外一個辦法就是由使用者來導引“仿真鏈接階段”的具現策略,決定哪些實體(instances)才是所需求的。
目前,不論是編譯時期還是鏈接時期的實例化(instantiation)策略,均存在以下弱點:當template實例被產生出來時,有時候會大量增加編譯時間。很顯然,這將是template functions第一次實例化時的必要條件。然而當那些函數被非必要地再次實例化,或是當“決定那些函數是否需要再實例化”所花的代價太大時,編譯器的表現令人失望
C++支持template的原始意圖可以想見是一個由使用者導引的自動實例化機制,既不需要使用者的介入,也不需要相同文件有多次的實例化行為。但是這已被證明是非常難以達成的任務,比任何人此刻所想象的還要難。
異常處理(Exception Handing)
欲支持exception handling,編譯器的主要工作就是找出catch子句,以處理被丟出來的exception。這多少需要追蹤程序堆棧中的每一個函數當前作用區域(包括追蹤函數中的local class objects當時的情況)。同時,編譯器必須提供某種查詢exception objects的方法,以知道其實際類型(這直接導致某種形式的執行期識別,也就是RTTI)。最后,還需要某種機制用以管理被丟出的object,包括它的產生、儲存、可能的解構(如果有相關的destructor)、清理(clean up)以及一般存取,也可能有一個以上的objects同時起作用。
一般而言,exception handling機制需要與編譯器所產生的數據結構以及執行期的一個exception library緊密合作,在程序大小和執行速度之間,編譯器必須有所抉擇:
- 為了維持執行速度,編譯器可以在編譯時期建立起用於支持的數據結構,這會使程序大小膨脹,但編譯器可以幾乎忽略這些結構,直到有個exception被丟出來。
- 為了維持程序大小,編譯器可以在執行期建立起用於支持的數據結構。這會影響程序的執行速度,但意味着編譯器只有在必要的時候才建立那些數據結構(並且可以拋棄之)。
Exception Handling 快速檢閱
C++的exception handing由三個主要的語匯組件構成:
- 一個throw子句。它在程序某處發出一個exception。被拋出去的expection可以是內建類型,也可以是使用者自定類型。
- 一個或多個catch子句。每一個catch子句都是一個exception handler。它用來表示說,這個子句准備處理某種類型的exception,並且在封閉的大括號區段中提供實際的處理程序
- 一個try區段。它被圍繞以一系列的敘述句(statements),這些敘述句可能會引發catch子句起作用
當一個exception被丟出去時,控制權會從函數調用中被釋放出來,並尋找一個吻合的catch子句。如果都沒有吻合者,那么默認的處理例程terminate()會被調用。當控制權被拋棄后,堆棧中的每一個函數調用也就被推離(popped up),這個程序稱為unwinding the stack。在每一個函數被推離堆棧之前,函數的local class objects的destructor會被調用。
對Exception Handling的支持
當一個exception發生時,編譯系統必須完成以下事情:
- 檢驗發生throw操作的函數;
- 決定throw操場是否發生在try區段中;
- 若是,編譯系統必須把exception type拿來和每一個catch子句比較;
- 如果比較吻合,流程控制應該交到catch子句手中;
- 如果throw的發生並不在try區段中,並沒有一個catch子句吻合,那么系統必須(a)摧毀所有active local objects,(b)從堆棧中將當前的函數"unwind"掉,(c)進行到程序堆棧中的下一個函數中去,然后重復上述步驟2~5
當一個實際對象在程序執行時被丟出,會發生什么事?
當一個exception被丟出時,exception object會被產生出來並通常放置在相同形式的exception數據堆棧中,從throw端傳染給catch子句的是exception object的地址、類型描述器(或是一個函數指針,該函數會傳回與該exception type有關的類型描述器對象),以及可能會有的exception object描述器(如果有人定義它的話)。
考慮一個catch子句如下:
catch(exPoint p){
//do something
throw;
}
以及一個exception object,類型為exVertex,派生自exPoint。這兩種類型都吻合,於是catch子句會作用起來。那么p會發生什么事?
- p將以exception object作為初值,就像是一個函數參數一樣。這意味着如果定義有(或由編譯器合成出)一個copy constructor和一個destructor的話,它們都會實施於local copy身上。
- 由於p是一個object而不是一個reference,當其內容被拷貝的時候,這個exception object的non-exPoint部分會被切掉(sliced off)。此外,如果為了exception的繼承而提供有virtual functions,那么p的vptr會被設為exPoint的virtual table;exception object的vptr不會被拷貝。
當這個exception被再丟出一次時,會發生什么事情呢?p是一個local object,在catch子句的末端將被摧毀。丟出p需得產生另一個臨時對象,並意味着喪失原來的exception的exVertex部分。原來的exception object被再一次丟出,任何對p的修改都會被拋棄。
像下面這樣的一個catch子句:
catch(exPoint &rp){
//do something
throw;
}
則是參考到真正的exception object。任何虛擬調用都會被決議(resolved)為instances active for exVertex,也就是exception object的真正類型。任何對此object的改變都會被復制到下一個catch子句中。
執行期類型識別(Runtime Type Identification, RTTI)
在cfront中,用以表現出一個程序所謂的“內部類型體系”,看起來像:
//程序層次結構的根類 root class
class node{ ... };
//root of 'type' subtree: basic types,
//'derived' types: points, arrays,
//functions, classes, enums, ...
class type : public node{ ... };
//two representations for functions
class fct : public type{ ... };
class gen : public type{ ... };
其中gen是generic的簡寫,用來表現一個overloaded function。
於是只要你有一個變量,或是類型為type*的成員(並知道它代表一個函數),你就必須決定其特定的derived type是否為fct或是gen。
Type-Safe Downcast(保證安全的向下轉型操作)
一個type-safe downcast(保證安全地向下轉換操作)必須在執行期對指針有所查詢,看看它是否指向它所展現(表達)之object的真正類型。因此,欲支持type-safe downcast在object空間和執行時間上都需要一些額外的負擔:
- 需要額外的空間以存儲類型信息(type information),通常是一個指針,指向某個類型信息節點
- 需要額外的時間以決定執行期的類型(runtime type),因為,正如其名所示,這需要再執行期才能決定
沖突發生在兩組使用者之間:
- 程序員大量使用多態(polymorphism),並因而需要正統而合法的大量downcast操作。
- 程序員使用內建數據類型以及非多態設備,因而不受各種額外負擔所帶來的報應。
理想的解決方案是:為兩派使用者提供正統而合法的需要——雖然或許得犧牲一些設計上的純度與優雅性。
C++的RTTI機制提供一個安全的downcast設備,但只對那些展現"多態(也就是使用繼承和動態綁定)"的類型有效。我們如何分辨這些?編譯器能否光看class的定義就決定這個class用以表現一個獨立的ADT或是一個支持多態的可繼承子類型(subtype)?當然,策略之一就是導入一個新的關鍵詞,優點是可以清楚地識別出支持新特性的類型,缺點則是必須翻新舊程序。
另一個策略是經由聲明一個或多個virtual functions來區別class聲明。其優點是透明化地將舊有程序轉化過來,只要重新編譯就好。缺點則是可能會將一個其實並非必要的virtual function強迫導入繼承體系的base class身上。在C++中,一個具備多態性質的class(所謂的polymorphic class),正是內含繼承而來(或是直接聲明)的virtual functions。
從編譯器的角度來看,這個策略還有其他優點,就是大量降低額外負擔。所有polymorphic classes的objects都維護了一個指針(vptr),指向virtual function table,只要我們把與該class相關的RTTI object地址放進virtual table中(通常放在第一個slot),那么額外負擔就降低為:每一個class object只多花費一個指針。這個指針只需被設定一次,它是被編譯器靜態設定,而不是在執行期由class constructor設定(vptr才是這么設定)。
Type-Safe Dynamic cast(保證安全的動態轉型)
dynamic_cast運算符可以在執行期決定真正的類型。如果downcast是安全的(也就是說,如果base type pointer指向一個derived class object),這個運算符會傳回被適當轉換過的指針。如果downcast不是安全地,這個運算符會傳回0
References並不是Pointers
程序中對一個class指針類型施以dynamic_cast運算符,會獲得true或false:
- 如果傳回真正的地址,表示這個object的動態類型被確認了,一些與類型相關的操作現在可以施行於其上。
- 如果傳回0,表示沒有指向任何object,意味應該以另一種邏輯施行於這個動態類型未確定的object身上。
dynamic_cast運算符也適用於reference身上。然而對於一個non-type-safe cast,其結果不會與施行於指針的情況相同。為什么?一個reference不可以像指針那樣"把自己設為0就代表了 no object";若將一個reference設為0,會引起一個臨時性對象(擁有被參考到的類型)被產生出來,該臨時對象的初值為0,這個reference然后被設定成為該臨時性對象的一個別名(alias)。
因此當dynamic_cast運算符施行於一個reference時,不能夠提供對等於指針情況下的那一組true/false。取而代之的是,會發生下列事情:
- 如果reference真正參考到適當的derived class(包括下一層或下下一層或下下下一層或...),downcast會被執行而程序可以繼續執行。
- 如果reference並不真正是某一種derived class,那么,由於不能傳回0,遂丟出一個bad_cast exception.
Typeid運算符
typeid運算符傳回一個const reference,類型為type_info。
type_info object由什么組成? C++ Standard中對type_info的定義如下:
class type_info{
public:
virtual ~type_info();
bool operator==(const type_info& ) const;
bool operator!=(const type_info& ) const;
bool before(const type_info&) const;
bool char* name() const; //傳回class原始名稱
private:
//prevent memberwise init and copy
type_info(const type_info& );
type_info& operator=(const type_info& );
//data members
};
編譯器必須提供的最小量信息是class的真實名稱、以及在type_info objects之間的某些排序算法(這就是before()函數目的)、以及某些形式的描述器,用以表現explicit class type和這個class的任何subtype。
雖然RTTI提供的type_info對於exception handling的支持來說是必要的,但對於exception handling的完整支持而言,還不夠。如果再加上額外一些type_info derived classes,就可以在exception發生時提供有關於指針、函數及類等等的更詳細信息。