這裏要討論三個著名的C++語言擴充性質,它們都會影響C++對象。它們分別是template、node
exception handling(EH)和runtime type identification(RTTI)。ios
C++程序設計的風格及習慣,自從1991年的cfront 3.0引入tempaltes以後就深深地改變了。原程序員
本template被視爲對container classes如Lists和Arrays的一項支持,但如今它已經成爲標準模板庫算法
(也就是Standard Template Library,STL)的基礎。它也被用於屬性混合(如內存配置策略或互express
斥(mutual exclusion)機制的參數技術之中。它甚至被用於一項所謂的template metaprogram技安全
術:class expression templates將在編譯時期而非執行期被評估,於是帶來重大的效率提高)。數據結構
下面是template的三個主要討論方向:架構
1)template的聲明。基原本說就是當聲明一個template class、template class member分佈式
function等待時,會發生什麼事情。ide
2)如何」實例化(instantiates)「class object、inline nonmember以及member template
functions。這些是」每個編譯單位都會擁有一份實例「的東西。
3)如何」實例化(instantiates)「nonmember、member tempalte functions以及static
template class members。這些都是」每個可執行文件中只須要一份實例「的東西。這就是通常
而言template所帶來的問題。
這裏使用」實例化「(instantiation)這個字眼來表示」進程(process)將真正的類型和表達式
綁定到template相關形式參數(formal parameters)上頭「的操做。舉個例子,下面是一個
template function:
template <class Type> Type min( const Type &t1, const Type &t2 ) { ... }
用法以下:
min( 1.0, 2.0 );
因而進程把Type綁定爲double併產生min()的一個程序文字實例(並施以」mangling「方法,給
它一個獨一無二的名稱),其中t1和t2的類型都是double。
考慮下面的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的某個實例來存取或操做。所以咱們能夠這樣
寫:
// ok: Point<float>::Status s;
但不能這樣寫:
// error: Point::Status s;
即便兩種類型抽象地來講是同樣的(並且,最理想的狀況下,咱們但願這個enum只有一個
實例被產生出來。若是不是這樣,咱們可能會想要把這個enum抽出到一個nontemplate base
class中,以免多份拷貝)。
一樣道理,freeList和chunkSize對程序而言也還不可用。咱們不可以寫:
// error : Point::freeList;
咱們必須顯式地指定類型,才能使用freeList:
// ok: Point<float>::freeList;
像上面這樣使用static member,會使其一份實例與Point class的float instantiation在程序中
產生關聯。若是咱們寫:
// ok : 另外一個實例(instance) Point<double>::freeList;
就會出現第二個freeList實例,與Point class的double instantiation產生關聯。
若是咱們定義一個指針,指向特定的實例,像這樣:
Point<float> *ptr = 0;
再一次,程序中什麼也沒發生。由於一個指向class object的指針,自己並非一個class
object,編譯器不須要知道與該class有關的任何members的數據或object佈局數據。因此將
「Point的一個float實例」實例化也就沒有必要了。在C++ Standard完成以前,「聲明一個指針指向
某個template class」這件事情並未被強制定義,編譯器能夠自行決定要或不要將template「實例
化」。cfront就是這麼作的!現在C++ Standard已經禁止編譯器這麼作。
若是不是pointer而是reference,又如何?假設:
const Point<float> &ref = 0;
它真的會實例化一個「Point的float實例」。這個定義的真正語意會被擴展爲:
// 內部擴展 Point<float> temporary( float ( 0 ) ); const Point<float> &ref = temporary;
由於reference並非無物(no object)的代名詞。0被視爲整數,必須被轉換爲如下類型的
一個對象:
Point <float>
若是沒有轉換的可能,這個定義就是錯誤的,會在編譯時被挑出來。
因此,一個class object的定義,不管是由編譯器暗中地作(像稍早程序代碼中出現過的
temporary),或是由程序員像下面這樣顯示地作:
const Point <float> origin;
都會致使template class的「實例化」,也就是說,float instantiation的真正對象佈局會被產生
出來。回顧先前的template聲明,咱們看到Point有三個nonstatic members,每個的類型都是
Type。Type如今被綁定爲float,因此origin的配置空間必須足夠容納三個float成員。
然而,member functions(至少對於那些未被使用過的)不該該被「實例化」。只有在
member functions被使用的時候,C++ Standard纔要求它們被「實例化」。目前的編譯器並不精確
遵循這項要求。之因此由使用者來主導「實例化」(instantiation)規則,有兩個主要緣由:
1)空間和時間效率的考慮。若是class中有100個member functions,但程序裏只針對某個
類型使用其中兩個,針對另外一個類型使用其中五個,那麼將其餘193個函數都「實例化」將花費大
量的時間和空間。
2)還沒有實現的機能。並非一個template實例化的全部類型就必定可以完整支持一組
member functions所須要的全部運算符。若是隻「實例化」那些真正用到的member
functions,template就可以支持那些本來可能會形成編譯時期錯誤的類型(types)。
舉個例子,origin的定義須要調用Point的default constructor和destructor,那麼只有這兩個
函數須要被「實例化」。相似的道理,當寫下:
Point <float> *p = new Point <float>;
時,只有(1)Point template的float實例、(2)new運算符、(3)default constructor須要
被「實例化」。有趣的是,雖然new運算符是這個class的一個implicitly static member,以致於它
不可以處理其中任何一個nonstatic members,但它仍是依賴真正的template參數類型,由於它
的第一參數size_t表明class的大小。
這些函數在何時「實例化」?目前流行兩種策略:
1)在編譯的時候,那麼函數將「實例化」於origin和p存在的那個文件中。
2)在連接的時候。那麼編譯器會被一些輔助工具從新激活。template函數實例可能被放在
這一文件中、別的文件中或一個分離的存儲位置。
在「int和long一致」(或「double和long double一致」)的架構之中,兩個類型實例化操做:
Point <int> pi; Point <long> pl;
應該產生一個仍是兩個實例呢?目前所知道的全部編譯器都產生兩個實例(可能有兩組
完整的member functions)。C++ Standard並未對此有什麼強制規定。
#include <iostream> template <class Type> class Point { public: enum Status { unallocated, normalized }; Point( Type x = 0.0, Type y = 0.0, Type z = 0.0 ) : _x( x ), _y( y ), _z( z ) { } ~Point() { } void* operator new( size_t size ) { return ::operator new( size ); } void operator delete( void* pointee ) { ::operator delete( pointee ); } Type y() { return _y; } // ... public: static Point<Type> *freeList; static int chunkSize; Type _x, _y, _z; }; int main() { Point<float> *ptr = 0; const Point<float> &ref = 0; Point<float> *p = new Point<float>; Point<int> pi; Point<long> pl; std::cout << "sizeof( *ptr ) = " << sizeof( *ptr ) << std::endl; //std::cout << "ptr->_x = " << ptr->_x << std::endl; std::cout << "sizeof( ref ) = " << sizeof( ref ) << std::endl; std::cout << "ref._x = " << ref._x << std::endl; std::cout << " &pi = " << &pi << std::endl; std::cout << " &pl = " << &pl << std::endl; }
當輸出「ptr->_x」時:
能夠看到,指針ptr的確未產生對象。
當輸出「ref._x」時:
能夠看到引用產生了對象實例。pi和pl是不一樣的實例。
在彙編生成的代碼中有:
_ZN5PointIfEC2Efff // Point<float>::Point(float, float, float) _ZN5PointIfED2Ev // int<float>::~Point() _ZN5PointIfEnwEj // Point<float>::operator new(unsigned int) _ZN5PointIiEC2Eiii // Point<int>::Point(int, int, int) _ZN5PointIiED2Ev // Point<int>::~Point() _ZN5PointIlEC2Elll // Point<long>::Point(long, long, long) _ZN5PointIlED2Ev // Point<long>::~Point()
能夠看到的確沒有產生y()函數和operator delete( void* pointee )函數的代碼,也產生了int
和long的兩組完整的member functions。
考慮下面的template聲明:
(1) template <class T> (2) class Mumble (3) { (4) public$: (5) Mumble( T t = 1024 ) (6) : _t( t ) (7) { (8) if( tt != t ) (9) throw ex ex; (10) } (11) private: (12) T tt; (13) }
這個Mumble template class的聲明內含一些既露骨又潛沉的錯誤:
1)L4:使用$4字符是不對的。這項錯誤有兩方面。第一,$並非一個能夠合法用於標識
符的字符;第二,class聲明中只容許有public、protected、private三個標籤(labels),$的出現
使public$不成爲public。第一點是語彙(lexical)上的錯誤,第二點則是造句/解析(syntactic
/parsing)上的錯誤。
2)L5:t被初始化爲整數常量1024,或許能夠,也或許不能夠,視T的真實類型而定。通常
而言,只有template的各個實例才診斷得出來。
3)L6:_t並非哪個member的名稱,tt纔是。這種錯誤通常會在「類型檢驗」這個階段被
找出來。是的,每個名稱必須綁定於一個定義身上,要不就會產生錯誤。
4)L8:!=運算符可能已定義好,但也可能尚未,視T的真正類型而定。和第二點一
樣,只有template的各個實例才診斷得出來。
5)L9:咱們意外地鍵入ex兩次。這個錯誤會在編譯時期的解析(parsing)階段被發現。
C++語言中一個合法的句子不容許一個標識符緊跟在另外一個標識符以後。
6)L13:咱們忘記了一個分號做爲class聲明的結束。這項錯誤也會在編譯時期的語句分析
(parsing)階段被發現。
在一個nontemplate class聲明中,這6個既露骨又潛沉的錯誤會被編譯器挑出來。但
template class卻不一樣。例如,全部與類型有關的檢驗,若是牽涉到template參數,都必須延遲
到真正實例化操做(instantiation)發生,才得爲之。也就是說,L5和L8的潛在錯誤會在每一個實
例操做(instantiation)發生時被檢查出來並記錄之,其結果將因不一樣的實際類型而不一樣。因而
結果:
#include <iostream> template <class T> class Mumble { public$: Mumble( T t = 1024 ) : _t( t ) { if( tt != t ) throw ex ex; } private: T tt; }; int main() { std::cout << "Hello World!" << std::endl; }
當只修改"public$"這一行時:
public:
修改"_t(t)"和"ex"兩行:
int ex; ... : tt( t ) ... throw ex;
經過編譯:
當在main函數裏添加:
Mumble<int> mi;
則L5和L8是正確的,編譯經過:
而若是:
Mumble<int*> pmi;
那麼L8正確L5錯誤,由於不可以將一個整數常量(除了0)指定給一個指針。
面對這樣的聲明:
class SmallInt { public: SmallInt( int _x ) : x( _x ) { } // ... private: int x; };
因爲其!=運算並未定義,因此下面的句子:
Mumble<SmallInt> smi;
會形成L8錯誤,而L5正確。固然,下面這個例子:
Mumble<SmallInt*> psmi;
又形成L8正確而L5錯誤。
那麼,什麼樣的錯誤會在編譯器處理template聲明時被標示出來?這有一部分和template的
處理策略有關。cfront對template的處理徹底解析(parse)但不作類型檢驗;只有在每個實例
化操做(instantiation)發生時才作類型檢驗。因此在一個parsing策略之下,全部語彙
(lexing)錯誤和解析(parsing)錯誤都會在處理template聲明的過程當中被標示出來。
語彙分析器(lexical analyzer)會在L4捕捉到一個不合法的字符,解析器(parser)會這
樣標示它:
public$: // caught
表示這是一個不合法的標籤(label)。解析器(parser)不會把「對一個未命名的member
做出參考操做」視爲錯誤:
_t( t ) // not caught
但它會抓出L9「ex出現兩次」以及L13「缺乏一個分號」這兩種錯誤。
在一個十分廣泛的代替策略中,template的聲明被收集成一系列的「lexical tokens」,而
parsing操做延遲直到真正有實例化操做(instantiation)發生時纔開始。每當看到一個
instantiation發生,這組token就被推往parser,而後調用類型檢驗,等等。面對先前出現的那個
template聲明,「lexical tokenizing」會指出什麼錯誤嗎?事實上不多,只有L4所使用的不合法字
符會被指出。其他的template聲明都被解析爲合法的tokens並被收集起來。
目前的編譯器,面對一個template聲明,在它被一組實際參數實例化以前,只能施行以有
限的錯誤檢查。template中那些與語法無關的錯誤,程序員可能認爲十分明顯,編譯器卻經過
了,只有在特定實例被定義以後,纔會發出抱怨。這是目前實現技術上的一個大問題。
Nonmember和member template functions在實例化行爲(instantiation)發生以前也同樣
沒有作完徹底的類型檢驗。這致使某些十分露骨的template錯誤聲明居然得以經過編譯。例如
下面的template聲明:
template<class type> class Foo { public: Foo(); type val(); void val( type v ); private: type _val; };
// bogus_member不是class的一個member function // dbx不是class的一個data member template <class type> double Foo<type>::bogus_member() { return this->dbx; }
在g++4.8.4中,編譯結果以下:
能夠看到class中的函數被顯示出錯誤。
若是在class中加入成員函數:
template<class type> class Foo { public: Foo(); type val(); void val( type v ); double bogus_member(); private: type _val; };
編譯經過,並不會報沒有dbx的錯誤。
這些都是編譯器設計者本身的決定。Template facility並無說不容許對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 ); // ... ScopeRules<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 the 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()候選者(注意,這種
行爲不可以以一個簡單的宏擴展——像是使用一個#define宏——重現之)。
讓咱們另外看看「與類型相關」(type-dependent)的用法:
sr0.type_dependent();
這個函數的內容以下:
return foo( _member );
這個例子很清楚地與template參數有關,由於該參數將決定_member的真正類型。因此這一
次foo()必須在「scope of the template instantiation」中決議,本例中這個scope有兩個foo()函數聲
明。因爲_member的類型在本例中爲int,因此應該是int版的foo()。若是ScopeRules以double
類型實例化,那麼就應該是double版的foo()出現。若是ScopeRules是以double類型實例化,那
麼該調用操做就曖昧不明。最後,若是ScopeRules以某一個class類型實例化,而該class沒有
針對int或double實現出convertion運算符,那麼foo()調用操做會被表示爲錯誤。無論如何演變,
都是由「scope of the template instantiation」來決定,而不是由「scope of the template
declaration」。
這意味着一個編譯器bib保持兩個scope contexts:
1)「scope of the template declarartion」,用以專一於通常的template class。
2)「scope of the template instantiation」,用以專一於特定的實例。
編譯器的決議(resolution)算法必須決定哪個纔是適當的scope,而後在其中搜索適當的
name。
對於template的支持,最困難的莫過於template function的實例化(instantiation)。目前編
譯器提供了兩個策略:一個是編譯時期策略,程序代碼在program text file中備妥可用:另外一個
是連接時期策略,有一些meta-compilation工具能夠導引編譯器的實例化行爲
(instantiation)。
下面是編譯器設計者必須回答的三個主要問題:
1)編譯器如何找出函數的定義?
答案之一是包含template program text file,就好像它是一個header文件同樣。Borland編
譯器就遵循這個策略。另外一種方法是要求一個文件命名規則,例如,咱們能夠要求,在Point.h
文件中發現的函數聲明,其template program text必定要放置於文件Point.C或Point.cpp中,依
此類推。cfront就遵循這個策略。Edison Design Group編譯器對這兩種策略都支持。
2)編譯器如何可以只實例化程序中用到的member functions?
解決辦法之一就是,根本忽略這項要求,把一個已經實例化的class的全部member
functions都產生出來。Borland就是這麼作的——雖然它也提供#pragmas能夠壓制(或實例
化)特定實例。另外一種策略就是模擬連接操做,檢測看看哪個函數真正須要,而後只爲它
(們)產生實例。cfront就是這麼作的。Edison Design Group編譯器對這兩種策略都支持。
3)編譯器如何阻止member definition在多個.o文件中都被實例化呢?
解決辦法之一就是產生多個實例,而後從連接器中提供支持,只留下其中一個實例,其他
都忽略。另外一個辦法就是由使用者來引導「模擬連接階段」的實例化策略,決定哪些實例
(instance)纔是所需求的。
目前,不管是編譯時期仍是連接時期的實例化(instantiation)策略,均存在如下弱點:
當template實例被產生出來時,有時候會大量增長編譯時間。很明顯,這將是template
functions第一次實例化時的必要條件。然而當那些函數被非必要地再次實例化,或是當「決定那
些函數是否須要再實例化」所花的代價太大時,編譯器的表現使人失望!
C++支持template的原始意圖能夠想見是一個由使用者導引的自動實例化機制(use-
directed automatic instantiation mechanism),既不須要使用者的介入,也不須要相同文件有
屢次的實例化行爲。可是這已被證實是很是難以達成的任務,比任何人此刻所能想象的還要
難。ptlink,隨着cfront3.0版所附的原始實例化工具,提供了一個由使用者驅動的自動實例化機
制(use-driven automatic instantiation mechanism),可是它實在太複雜了,即便是久經世故
的人也沒辦法一會兒瞭解。
Edison Design Group開發出一套第二代的directed-instantiation機制,很是接近於
template facility原始含義。它主要運做以下:
1)一個程序的原始碼被編譯時,最初並不會產生任何「template實例化」。然而,相關信息
已經被產生於object files之中。
2)當object files被連接在一塊時,會有一個prelinker程序被執行起來。它會檢查object
files,尋找template實例的相互參考以及對應的定義。
3)對於每個「參考到template實例」而「該實例卻沒有定義」的狀況,prelinker將該文件視爲
與另外一個實例化(在其中,實例已經實例化)等同。以這種方法,就能夠將必要的程序實例化
操做指定給特定的文件。這些都會註冊在prelinker所產生的.ii文件中(放在磁盤目錄ii_file)。
4)prelinker從新執行編譯器,從新編譯每個「.ii文件曾被改變過」的文件。這個過程不斷
重複,直到全部必要的實例化操做都已完成。
5)全部的object files被連接成一個可執行文件。
這種direct-instantiation體制的主要成本在於,程序第一次被編譯時的.ii文件設定時間。次
要成本則是必須針對每個「complie afterwards」執行prelinker,以確保全部被參考到的
templates都存在着定義。在最初的設計以及成功地第一次連接以後,從新編譯操做包含如下程
序:
1)對於每個將被從新編譯的program text file,編譯器檢查其對應的.ii文件。
2)若是對應的.ii文件列出一組要被實例化(instantiated)的templates,那些templates(而
且只有那些templates)會在此編譯時被實例化。
3)prelinker必須執行起來,確保全部被參考到的template已經被定義穩當。
出現某種形式的automated template機制,是「對程序員友善的C++編譯系統」的一個必要組
件。
不幸的是,沒有任何一個機制是沒有bugs的。Edison Design Group的編譯器使用了一個由
cfront2.0引入的算法,針對程序中的每個class自動產生virtual table的單一實例。例以下面的
class聲明:
class PrimitiveObject : public Geometry { public: virtual ~PrimitiveObject(); virtual void draw(); ... };
若是它被含入於15個或45個程序源碼中,編譯器如何可以確保只有一個virtual table實例被
產生出來呢?產生15份或45份實例倒還容易些!
Koenig如下面的方法解決這個問題:每個virtual function的地址都被放置於active classes
的virtual table中。若是取得函數地址,表示virtual function的定義一定出如今程序的某個地點;
不然程序就沒法連接成功。此外,此函數只能有一個實例,不然也是連接不成功。那麼,就把
virtual table放在定義了該class之第一個non-inline、nonpure virtual function的文件中。以咱們
的例子而言,編譯器會將virtual table產生在存儲着virtual destructor的文件之中。
不幸的是,在template之中,這種單必定義並不必定爲真。在template所支持的「將模塊中
的每同樣東西都編譯」的模型下,不僅是多個定義可能被產生,並且連接器也聽任讓多個定義同
時出現,它只要選擇其中一個而將其他都忽略,也就是了。
但Edison Design Group的automatic instantiation機制作什麼事呢?考慮下面這個library函
數:
void foo( const Point<float> *ptr ) { ptr->virtual_func(); }
virtual function call被轉換爲相似這樣的東西:
// C++僞碼 // ptr->virtual_func(); ( *ptr->_vtbl_Point<float>[ 2 ] )( ptr );
因而致使實例化(instantiated)Point class的一個float實例及其virtual_func()。因爲每個
virtual function的地址被放置於table之中,若是virtual table被產生出來,每個virtual function
也都必須被實例化(instantiated)。這就是爲何C++ Standard有下面的文字說明的緣故:
若是一個vitual function被實例化(instantiated),其實例化點緊跟在其class的實例化點之
後。
然而,若是編譯器遵循cfront的virtual table實現體制,那麼在」Point的float實例有一個virtual
destructor定義被實例化「以前,這個table不會被產生。除非,在這一點上,並無顯式使用
virtual destructor以擔保其實例化行爲(instantiation)。
Edison Design Group的automatic template機制並不明白它本身的編譯器對第一個non-
inline、nonpure virtual function的隱式使用,因此並無把它標於.ii文件中。結果,連接器反而
回頭抱怨下面這個符號沒有出現:
_vtbl_Point<float>
並拒絕產生一個可執行文件。Automatic instantiation在此失效!程序員必須顯式地強迫將
destructor實例化。目前的編譯系統以#program指令來支持此需求。然而C++ Standard也已經
擴充了對template的支持,容許程序員顯式地要求在一個文件中將整個class template實例化:
template class Point3d<float>;
或是針對一個template class的個別member function:
template float Point3d<float>::X() const;
或是針對一個個別template function:
template Point3d<float> operator+ ( const Point3d<float>&, const Point3d<float>& );
實際上,template instantitation彷佛拒絕了全面的自動化。甚至雖然每一件工做都作對了,
產生出來的object files的從新編譯成本仍然可能很高——若是程序十分巨大的話!以手動方式先
在個別的object module中完成預先實例化操做(pre-instantiation),雖然沉悶,倒是惟一有效
率的作法。
欲支持exception handling,編譯器的主要工做就是找出catch子句,以處理被拋
出來的exception。這多少須要追蹤程序堆棧中的每個函數的目前做用區域(包括
追蹤函數中local class objects當時的狀況)。同時,編譯器必須提供某種查詢
exception objects的方法,以知道其實際類型(這直接致使某種形式的執行期類型識
別,也就是RTTI)。最後,還須要某種機制用以管理被拋出的object,包括它的產
生、存儲、可能的析構(若是有相關的destructor)、清理(clean up)以及通常存
取。也可能有一個以上的objects同時起做用。通常而言,exception handling機制需
要與編譯器所產生的數據結構以及執行期的一個exception library緊密合做。在程序
大小和執行速度之間,編譯器必須有所抉擇:
1)爲了維護執行速度,編譯器能夠在編譯時期創建起用於支持的數據結構。這
會使程序的大小發生膨脹,但編譯器能夠幾乎忽略這些結構,直到exception被拋
出。
2)爲了維護程序大小,編譯器能夠在執行期創建起用於支持的數據結構。這會影響程序的
執行速度,但意味着編譯器只有在必要的時候才創建那些數據結構(而且能夠拋棄之)。
C++的exception handling由三個主要的語彙組件構成:
1)一個throw子句。它在程序某處發出一個exception。被拋出去的exception能夠是內建類
型,也能夠是使用者自定類型。
2)一個或多個catch子句。每個catch子句都是一個exception handler。它用來表示說,這
個子句準備處理某種類型的exception,而且在封閉的大括號區段中提供實際的處理程序。
3)一個try區段。它被圍繞以一系列的敘述句(statements),這些敘述句可能會引起catch
子句起做用。
當一個exception被拋出去時,控制權會從函數調用中被釋放出來,並尋找一個吻合的catch
子句。若是沒有吻合者,那麼默認的處理例程terminate()會被調用。當控制權被放棄後,堆棧中
的每個函數調用也就被推離(popped up)。這個程序稱爲unwinding the stack。在每個函
數被推離堆棧以前,函數的local class objects的destructor會被調用。
Exception handling 中比較不那麼直覺的就是它對於那些彷佛沒什麼事作的函數所帶來的衝
擊。例以下面這個函數:
(1) Point* (2) mumble() (3) { (4) Point *ptl, *pt2; (5) pt1 = foo(); (6) if( !pt1 ) (7) return 0; (8) (9) Point p; (10) (11) pt2 = foo(); (12) if( !pt2 ) (13) return pt1; (14) (15) ... (16) }
若是有一個exception在第一次調用foo()(L5)時被拋出,那麼這個mumble()函數會被推出程
序堆棧。因爲調用foo()的操做並不在一個try區段以內,也就不須要嘗試和一個catch子句吻合。
這裏也沒有任何local class objects須要析構。然而若是有一個exception在第二次調用foo()
(L11)時被拋出,exception handling機制就必須在」從程序堆棧中」unwindling「這個函數「之
前,先調用p的destructor。
在exception handling之下,L4~L8和L9~L16被視爲兩塊語意不一樣的區域,由於當exception
被拋出來時,這兩塊區域有不一樣的執行期語意。並且,欲支持exception handling,須要額外的
一些」薄記「操做與數據。編譯器的作法有兩種:一種是把兩塊區域以個別的」將被摧毀之local
objects「鏈表(已在編譯時期設妥)聯合起來;另外一種作法是讓兩塊區域共享同一個鏈表,該鏈表
會在執行期擴大或縮小。
在程序員層面,exception handling也改變了函數在資源管理上的語意。例如,下面的函數
中含有對一塊共享內存的locking和unlocking操做,雖然看起來和exceptions沒有什麼關係,但
在exception handling之下並不保證可以正確容許:
void mumble( void *arena ) { Point *p = new Point; smLock( arena ); // function call // 若是有一個exception在此發生,問題就來了 // ... smUnLock( arena ); // function call delete p; }
本例之中,exception handling機制把整個函數視爲單一區域,不須要操心」將函數從程序堆
棧中「unwinding」的事情。然而從語意上來講,在函數被推出堆棧以前,咱們須要unlock共享內
存,並delete p。讓函數稱爲「exception proof」的最明確(但不是最有效率)方法就是安插一個
default catch子句,像這樣:
void mumble( void *arena ) { Point *p p = new Point; try { smLock( arena ); // function call // ... } catch( ... ) { smUnLock( arena ); delete p; throw; } smUnLock( arena ); delete p; }
這個函數如今有了兩個區域:
1)try block之外的區域,在那裏,exception handling機制除了「pop」程序堆棧以外,沒有其
他事情要作。
2)try block之內的區域(以及它所聯合的default catch子句)。
請注意,new運算符的調用並不是在try區段內。若是new運算符或是Point constructor在配置內
存以後發生一個exception,那麼內存既不會被unlocking,p也不會被delete(這兩個操做都在
catch區段內)。這是正確的語意嗎?
是的,它是。若是new運算符拋出一個exception,那麼就不須要配置heap中的內存,Point
constructor也不須要被調用。因此也就沒有理由調用delete運算符。然而若是是在Point
constructor中發生exception,此時內存已配置完成,那麼Point之中任何構建好的合成物或子對
象(subobject,也就是一個member class object或base class object)都將自動被析構掉,然
後heap內存也會被釋放掉。不論哪一種狀況,都不須要delete運算符。
相似的道理,若是一個exception是在new運算符執行過程當中被拋出的,arena所指向的內存
就毫不會被locked,所以,也沒有必要unlock之。
處理這些資源管理問題,一個建議辦法就是,將資源需求封裝於一個class object體內,並由
destructor來釋放資源(然而若是資源必須被索求、被釋放、再被索求、再被釋放......許屢次的
時候,這種風格會變得優勢累贅):
void mumble( void *arena ) { auto_ptr<Point> ph ( new Point ); SMLock sm( arena ); // 若是這裏拋出一個exception,如今就沒有問題了 // ... // 不須要顯式地unlock和delete // local destructors在這裏被調用 // sm.SMLock::~SMLock(); // ph.auto_ptr<Point>::~auto_ptr<Point>() }
從exception handling的角度看,這個函數如今有三個區段:
1)第一區是auto_ptr被定義之處。
2)第二區段是SMLock被定義之處。
3)上述兩個定義以後的整個函數。
若是exception是在auto_ptr constructor中被拋出的,那麼就沒有active local objects須要被
EH機制摧毀。然而若是SMLock constructor中拋出一個exception,auto_ptr object必須在
「unwinding」以前先被摧毀。至於在第三個區段中,兩個local objects固然都必須被摧毀。
支持EH,會使那些擁有member class subobjects或base class subobjects(而且它們也都
有constructors)的classes的constructor更復雜。一個class若是被部分構造,其destructor必須
只施行於那些已被構造的subobjets和(或)member objects身上。例如,假設class X有
member objects A, B和C,都各有一對constructor和destructor,若是A的constructor拋出一個
exception,不論A、B或C都不須要調用其destructor。若是B的constructor拋出一個
exception,A的destructor必須被調用,但C不用。處理全部這些意外事故,是編譯器的責任。
一樣的道理,若是程序員寫下:
// class Point3d : public Point2d { ... } Point3d *cvs = new Point3d[ 512 ];
會發生兩件事:
1)從heap中配置足以給512個Point3d objects所用的內存。
2)若是成功,先是Point2d constructor,而後是Point3d constructor,會施行於每個元素
身上。
若是#27元素的Point3d constructor拋出一個exception,會怎樣?對於#27元素,只有
Point2d destructor須要調用執行。對於前26個元素,Point3d destructor和Point2d destructor都
須要調用執行。而後內存必須被釋放回去。
二、對Exception Handling的支持
當一個exception發生時,編譯系統必須完成如下事情:
1)檢驗發生throw操做的函數。
2)決定throw操做是否發生在try區段中。
3)如果,編譯系統必須把exception type拿來和每個catch子句進行比較。
4)若是比較後吻合,流程控制應該交到catch子句手中。
5)若是throw的發生並不在try區段中,或沒有一個catch子句吻合,那麼系統必須(a)摧毀
全部active local objects,(b)從堆棧中將目前的函數「unwind」掉,(c)進行到程序堆棧的下
一個函數中去,而後重複上述步驟2~5。
一個函數能夠被想象爲好幾個區域:
1)try區段之外的區域,並且沒有active local objects。
2)try區段之外的區域,但有一個(或以上)的active local objects須要析構。
3)try區段之內的區域。
編譯器必須表示出以上各區域,並使它們對執行期的exception handling系統有所做用。一個
很棒的策略就是構造出program counter-range表格。
program counter(EIP寄存器)內含下一個即將執行的程序指令。爲了在一個內含try區段的
函數中表示出某個區域,能夠把program counter的起始值和結束值(或是起始值和範圍)存儲
在一個表格中。
當throw操做發生時,目前的program counter值被拿來與對應的「範圍表格」進行對比,比決
定目前做用中的區域是否在一個try區域中。若是是,就須要找出相關的catch子句。若是這個
exception沒法被處理(或者它被再次拋出),目前的這個函數會從程序中被推出(popped),
而program counter會被設定爲調用端地址,而後這樣的循環再從新開始。
對於每個被拋出來的exception,編譯器必須產生一個類型描述器,對exception的類型進
行編碼。若是那是一個derived type,編碼內容必須包括其全部base class的類型信息。只編進
public base class的類型是不夠的,由於這個exception可能被一個member function捕捉,而在
一個member function的範圍(scope)之中,derived class和nonpublic base class之間能夠轉
換。
類型描述器(type descriptor)是必要的,由於真正的exception是在執行期被處理的,其
object必須有本身的類型信息。RTTI正是由於支持EH而得到的副產品。
編譯器還必須爲每個catch子句產生一個類型描述器。執行期的exception handler會將「被
拋出之object的類型描述器」和「每個cause子句的類型描述器」進行比較,直到找到吻合的一
個,或是直到堆棧已經被「unwound」而terminate()已被調用。
每個函數會產生一個exception表格,它描述與函數相關的各區域,任何須要的善後處理
代碼(cleanup code,被local class object destructors調用)以及catch子句的位置(若是某個
區域是在try區段之中的話)。
當一個exception被拋出時,exception object會被產生出來並一般放置在相同形式的
exception數據堆棧中。從throw端傳給catch子句的,是exceotion object的地址、類型描述器
(或是一個函數指針,該函數會傳回與該exception type有關的類型描述器對象)以及可能會有
的exception object描述器(若是有人定義它的話)。
考慮一個catch子句以下:
catch ( exPoint p ) { // do something throw; }
以及一個exception object,類型爲exVertex,派生自exPoint。這兩種類型都吻合,因而
catch子句會做用起來。那麼p會發生什麼事?
1)p將以exception object做爲初值,就像一個函數參數同樣。這意味着若是定義有(或由
編譯器合成出)一個copy constructor和一個destructor的話,它們都會實施於local copy身上。
2)因爲p是一個object而不是一個reference,當其內容被拷貝的時候,這個exception
object的non-exPoint部分會被切掉(sliced off)。此外,若是爲了exception的繼承而提供
virtual function,那麼p的vptr會被設爲exPoint的virtual table;exception object的vptr不會拷貝。
當這個exception被再拋出一次時,會發生什麼事情?p如今是繁殖出來的object?仍是從
throw端產生的原始exception object?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子句。
最後,這裏提出一個有趣的謎題。若是咱們有下面的throw操做:
exVertex errVer; // ... mumble() { // ... if( mumble_cond ) { errVer.fileName( "mumble()" ); throw errVer; } // ... }
到底是真正的exception errVer被繁殖,仍是errVer的一個複製品被構造於exception stack之
中並不被繁殖?答案是一個複製品被構造出來,全局性的errVer並無被繁殖。這意味着在一個
catch子句中對於exception object的任何改變都是局部性的,不會影響errVer。只是在一個catch
子句評估完畢而且知道它不會再拋出excption以後,真正的exception object纔會被摧毀。
在cfront中,用以表現出一個程序的所謂「內部類型體系」,看起來像這樣:
// 程序層次結構的根類(root class) class node { ... }; // root of the 'type' subtree:basic types, // 'derived' types: pointers, arrays, // functions, classes, enums ... class type : public node { ... }; // two representtations for functions class fct : public type { ... }; class gen : public type { ... };
其中gen是generic的簡寫,用來表現一個overloaded function。
因而只要你有一個變量,或是類型爲type*的成員(並知道它表明一個函數),你就必須決定
其特定的derived type是否爲fct或是gen。在2.0以前,除了destructor以外惟一不可以被overload
的函數就是conversion運算符,例如:
class String { public: operator char*(); // ... };
在2.0導入const member functions以前,conversion運算符不可以被overload,由於它們不
使用參數。直到引進了const member functions,狀況纔有所變化。如今,像下面這樣的聲明就
可能了:
class String { public: // ok with Release 2.0 operator char*(); operator char*() const; // ... };
也就是說,在2.0版本以前,以一個explicit cast來存取derived object老是安全(並且比較快
速)的,像下面這樣:
typedef type *ptype; typedef fct *pfct; simlipy_conv_op( ptype pt ) { // ok : conversion operators can only be fcts pfct pf = pfct( pt ); // ... }
在const member functions引入以前,這份代碼是正確的。但以後就不對了。是由於String
class聲明的改變,由於char* conversion運算符如今被內部視爲一個gen而不是一個fct。
下面這樣的轉換形式:
pfct pf = pfct( pt );
被稱爲downcast(向下轉換),由於它有效地把一個base class轉換至繼承架構的末端,變
成其derived classes中某一個Downcast有潛在性的危險,由於它遏制了類型系統的做用,不正
確的使用可能會帶來錯誤的解釋(若是它是一個read操做)或腐蝕掉程序內存(若是它是一個
write操做)。在咱們的例子中,一個指向gen object的指針被不正確地轉換爲一個指向fct object
的指針pf。全部後續對pf的使用都是不正確的(除非只是檢查它是否爲0,或只是把它拿來和其
他指針進行比較)。
一、Type-Safe Downcast(保證安全的向下轉換操做)
C++被吹毛求疵的一點就是,它缺少一個保證安全的downcast(向下轉換操做)。只有在「類
型真的能夠被適當轉換」的狀況下,纔可以執行downcast。一個type-safe downcast必須在執行
期有所查詢,看看它是否指向它所展示(表達)之object的真正類型。所以,欲支持type-safe
downcast,在object空間和執行時間上都須要一些額外負擔:
1)須要額外的空間以存儲類型信息(type information),一般是一個指針,指向某個類型信
息節點。
2)須要額外的空間以決定執行期的類型(runtime type),由於,正如其名所示,這須要在
執行期才能決定。
這樣的機制面對下面這樣日常的C結構,會如何影響其大小、效率以及連接兼容性呢?
char *winnie_tbl[] = { "rumbly in my tummy", "oh, bother" };
它所致使的空間和效率上的不良後果甚爲可觀。
衝突發生在兩組使用者之間:
1)程序員大量使用多態(polymorphism),並於是須要正統而合法的大量downcast操做。
2)程序員使用內建數據類型以及非多態設備,於是不受各類額外負擔所帶來的不良後果。
理想的解決方案是,爲兩派使用者提供正統而合法的須要——雖然或許得犧牲一些設計上的
純度與優雅性。
C++的RTTI機制提供了一個安全的downcast設備,但支隊那些展示」多態(也就是使用繼承
和動態綁定)「的類型有效。咱們如何分辨這些?編譯器可否光看class 的定義就決定這個class
用以表現一個獨立的ADT仍是一個支持多態的可繼承子類型(subtype)?固然,策略之一就是
導入一個新的關鍵詞,優勢是能夠清楚地識別支持新特性的類型,缺點則是必須翻新舊的程
序。
另外一個策略是經過聲明一個或多個virtual functions來區別class聲明。其優勢是透明化地將舊
有程序轉換過來,只要從新編譯就好。缺點則是可能會將一個起始並不是必要的virtual function強
迫導入繼承體系的base class身上。這正是目前RTTI機制所支持的策略。在C++中,一個具有
多態性質的class(所謂的pilymorphic class),正式內含着繼承而來(或直接聲明)的virtual
functions。
從編譯器的角度來講,這個策略還有其餘優勢,就是大量下降額外負擔。全部polymorphic
classes的objects都維護了一個指針(vptr),指向virtual function table。只要咱們把與該class
相關的RTTI object地址放進virtual table(一般是第一個slot),那麼額外負擔就下降爲:每一
個class object只多花費一個指針。這一指針只須要被設定一次,它是被編譯器靜態設定的,而
非在執行期由class constructor設定(vptr纔是這麼設定的)。
dunamic_cast運算符能夠在執行期決定真正的類型。若是downcast是安全的(也就是說,
若是base type pointer指向一個derived class object),這個運算符會傳回適當轉換過的指針。
若是downcast不是安全的,這個運算符會傳回0。下面就咱們如何重寫咱們本來的cfront
downcast:
typedef type *ptype; typedef fct *pfct; simplify_conv_op( ptype pt ) { if( pgct pf = dynamic_cast<pfct>( pt ) ) { // ...process of } else { ... } }
什麼是dynamic_cast的真正成本呢?pfct的一個類型描述器會被編譯器產生出來。由pt所指向
的class object類型描述器必須在執行期經過vptr取得。下面就是可能的轉換:
// 取得pt的類型描述器 ( ( type_info* )( pt->vptr[ 0 ] ) )->_type_descriptor;
type_info是C++ Standard所定義的類型描述器的class名稱,該class中放置着帶索求的類型
信息。virtual table的第一個slot內含type_info object的地址;此type_info object與pt所指的class
type有關。這兩個類型描述器被交給一個runtime library函數,比較以後告訴咱們是否吻合。很
顯然這筆static cast昂貴得多,但卻安全得多(若是咱們把一個fct類型」downcast」爲一個gen類
型的話)。
最初對runtime cast的支持提議中,並未引進任何關鍵詞或額外的語法。下面這樣的轉換操
做:
// 最初對runtime cast的提議語法 pfct pf = pfct( pt );
到底是static仍是dynamic,必須視pt是否指向一個多態class object而定。
程序執行中對一個class指針類型施以dynamic_cast運算符,會得到true或false:
1)若是傳回真正的地址,則表示這一object的動態類型被確認了,一些與類型有關的操做現
在能夠施行於其上。
2)若是傳回0,則表示沒有指向任何object,意味着應該以另外一種邏輯施行於這個動態類型未
肯定的object身上。
dynamic_cast運算符也使用於reference身上。然而對於一個non-type-safe cast,其結果不會
與施行於指針的狀況相同。爲何?一個reference設爲0,會引發一個臨時性對象(擁有被參考
的類型)被產生出來,該臨時對象的初值爲0,這個reference而後被設定成爲該臨時對象的一個
別名(alias)。所以當dynamic_cast運算符施行於一個reference時,不可以提供對等於指針情
況下的那一組true/false。取而代之的是,會發生下列事情:
1)若是reference真正參考到適當的derived class(包括下一層或下下一層,或下下下一層
或...),downcast會被執行而程序能夠繼進行。
2)若是reference並不真正是某一種derived class,那麼,因爲不可以傳回0,所以拋出一個
bad_cast exception。
下面是從新實現後的simplify_conv_op函數,參數改成一個reference:
simplify_conv_op( const type &rt ) { try { fct &rf = dynamic_cast<fct&>( rt ); // ... } catch( bad_cast ) { // ... mumble ... } }
其中執行的操做十分理想地表現出某種exception failure,而不知是簡單(一如從前)的控制
流程。
使用typeid運算符,就有可能以一個reference達到相同的執行期代替路線(runtime
"alternative pathway"):
simplify_conv_op( const type &rt ) { if( typeid( rt ) == typeid( fct ) ) { fct &rf = static_cast<fct&>( rt ); // ... } else { ... } }
在這裏,一個明顯的較好實現策略是在gen和fctlasses中都引進一個virtual function。
typeid運算符傳回一個const reference,類型爲type_info。在先前測試中出現的equlity(等
號)運算符,實際上是一個被overloaded的函數:
bool type_info:: operator==( const type_info& ) const;
若是兩個type_info objects相等,這個equality運算符就傳回true。
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; const 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()函數的目的),以及某些形式的描述器,用來表現eplicit class type和這一
class的任何subtypes。
雖然RTTI提供的type_info對於exception handling的支持是必要的,但對於exception
handling的完整支持而言,還不夠。若是再加上額外的一些type_info derived classes,就能夠
在exception發生時提供關於指針、函數、類等等的更詳細信息。例如MetaWare就定義瞭如下
的額外類:
class Pointer_type_info : public type_info { ... }; class Member_pointer_info : public type_info { ... }; class Modified_type_info : public type_info { ... }; class Array_type_info : public type_info { ... }; class Func_type_info : public type_info { ... }; class Class_type_info : public type_info { ... };
並容許使用者取用它們。RTTI只適用於多態類(ploymorphic classese),事實上type_info
objects也適用於內建類型,以及非多態的使用者自定類型。這對於exception handling的支持是
有必要的。例如:
int ex_errno; ... throw ex_errno;
其中int類型也有它本身的type_info object。下面就是使用方法:
int *ptr; ... if( typeid( ptr ) == typeid( int* ) ) ...
在程序中使用typeid(expression),像這樣:
int ival; ... typeid( ival ) ...;
或是使用typeid( type ),像這樣:
typeid( double ) ...;
會傳回一個const type_info&。這與先前使用多態類型(polymorphic types)的差別在於,這
時候的type_info object是靜態取得,而非執行期取得。通常的實現策略是在須要時才產生
type_info object,而非程序一開頭就產生之。
傳統的C++對象模型提供有效率的執行期支持。這份效率,再加上與C之間的兼容性,形成了
C++的普遍被接受度。然而,在某些領域方面,像是動態共享函數庫(dynamically shared
libraries)、共享內存(shared memory)以及分佈式對象(distrubuted object)方面,這個對
象模型的彈性仍是不夠。