C++对象模型学习——站在对象模型的尖端
2019獨(dú)角獸企業(yè)重金招聘Python工程師標(biāo)準(zhǔn)>>>
?????? 這里要討論三個(gè)著名的C++語言擴(kuò)充性質(zhì),它們都會(huì)影響C++對(duì)象。它們分別是template、
exception handling(EH)和runtime type identification(RTTI)。
一、Template
???? C++程序設(shè)計(jì)的風(fēng)格及習(xí)慣,自從1991年的cfront 3.0引入tempaltes之后就深深地改變了。原
本template被視為對(duì)container classes如Lists和Arrays的一項(xiàng)支持,但現(xiàn)在它已經(jīng)成為標(biāo)準(zhǔn)模板庫
(也就是Standard Template Library,STL)的基礎(chǔ)。它也被用于屬性混合(如內(nèi)存配置策略或互
斥(mutual exclusion)機(jī)制的參數(shù)技術(shù)之中。它甚至被用于一項(xiàng)所謂的template metaprogram技
術(shù):class expression templates將在編譯時(shí)期而非執(zhí)行期被評(píng)估,因而帶來重大的效率提升)。
????? 下面是template的三個(gè)主要討論方向:
???? 1)template的聲明。基本來說就是當(dāng)聲明一個(gè)template class、template class member
function等待時(shí),會(huì)發(fā)生什么事情。
???? 2)如何”實(shí)例化(instantiates)“class object、inline nonmember以及member template
functions。這些是”每一個(gè)編譯單位都會(huì)擁有一份實(shí)例“的東西。
???? 3)如何”實(shí)例化(instantiates)“nonmember、member tempalte functions以及static
template class members。這些都是”每一個(gè)可執(zhí)行文件中只需要一份實(shí)例“的東西。這就是一般
而言template所帶來的問題。
???? 這里使用”實(shí)例化“(instantiation)這個(gè)字眼來表示”進(jìn)程(process)將真正的類型和表達(dá)式
綁定到template相關(guān)形式參數(shù)(formal parameters)上頭“的操作。舉個(gè)例子,下面是一個(gè)
template function:
template <class Type> Type min( const Type &t1, const Type &t2 ) { ... }????? 用法如下:
min( 1.0, 2.0 );???? 于是進(jìn)程把Type綁定為double并產(chǎn)生min()的一個(gè)程序文字實(shí)例(并施以”mangling“方法,給
它一個(gè)獨(dú)一無二的名稱),其中t1和t2的類型都是double。
???? 1、Template的”實(shí)例化“行為(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; };???? 首先,當(dāng)編譯器看到template class聲明時(shí),它會(huì)做出什么反應(yīng)?在實(shí)際程序中,什么反應(yīng)也
沒有!也就是說,上述的static data members并不可用。nested enum或其enumerators也一
樣。
???? 雖然enum Status的真正類型在所有的Point instantiations中都一樣,其enumerators也是,
但它們每一個(gè)都只能夠通過template Point class的某個(gè)實(shí)例來存取或操作。因此我們可以這樣
寫:
// ok: Point<float>::Status s;????? 但不能這樣寫:
// error: Point::Status s;????? 即使兩種類型抽象地來說是一樣的(而且,最理想的情況下,我們希望這個(gè)enum只有一個(gè)
實(shí)例被產(chǎn)生出來。如果不是這樣,我們可能會(huì)想要把這個(gè)enum抽出到一個(gè)nontemplate base
class中,以避免多份拷貝)。
????? 同樣道理,freeList和chunkSize對(duì)程序而言也還不可用。我們不能夠?qū)?#xff1a;
// error : Point::freeList;????? 我們必須顯式地指定類型,才能使用freeList:
// ok: Point<float>::freeList;????? 像上面這樣使用static member,會(huì)使其一份實(shí)例與Point class的float instantiation在程序中
產(chǎn)生關(guān)聯(lián)。如果我們寫:
// ok : 另一個(gè)實(shí)例(instance) Point<double>::freeList;???? 就會(huì)出現(xiàn)第二個(gè)freeList實(shí)例,與Point class的double instantiation產(chǎn)生關(guān)聯(lián)。
???? 如果我們定義一個(gè)指針,指向特定的實(shí)例,像這樣:
Point<float> *ptr = 0;???? 再一次,程序中什么也沒發(fā)生。因?yàn)橐粋€(gè)指向class object的指針,本身并不是一個(gè)class
object,編譯器不需要知道與該class有關(guān)的任何members的數(shù)據(jù)或object布局?jǐn)?shù)據(jù)。所以將
“Point的一個(gè)float實(shí)例”實(shí)例化也就沒有必要了。在C++ Standard完成之前,“聲明一個(gè)指針指向
某個(gè)template class”這件事情并未被強(qiáng)制定義,編譯器可以自行決定要或不要將template“實(shí)例
化”。cfront就是這么做的!如今C++ Standard已經(jīng)禁止編譯器這么做。
????? 如果不是pointer而是reference,又如何?假設(shè):
const Point<float> &ref = 0;????? 它真的會(huì)實(shí)例化一個(gè)“Point的float實(shí)例”。這個(gè)定義的真正語意會(huì)被擴(kuò)展為:
// 內(nèi)部擴(kuò)展 Point<float> temporary( float ( 0 ) ); const Point<float> &ref = temporary;????? 因?yàn)閞eference并不是無物(no object)的代名詞。0被視為整數(shù),必須被轉(zhuǎn)換為以下類型的
一個(gè)對(duì)象:
Point <float>?????? 如果沒有轉(zhuǎn)換的可能,這個(gè)定義就是錯(cuò)誤的,會(huì)在編譯時(shí)被挑出來。
?????? 所以,一個(gè)class object的定義,不論是由編譯器暗中地做(像稍早程序代碼中出現(xiàn)過的
temporary),或是由程序員像下面這樣顯示地做:
const Point <float> origin;?????? 都會(huì)導(dǎo)致template class的“實(shí)例化”,也就是說,float instantiation的真正對(duì)象布局會(huì)被產(chǎn)生
出來?;仡櫹惹暗膖emplate聲明,我們看到Point有三個(gè)nonstatic members,每一個(gè)的類型都是
Type。Type現(xiàn)在被綁定為float,所以origin的配置空間必須足夠容納三個(gè)float成員。
?????? 然而,member functions(至少對(duì)于那些未被使用過的)不應(yīng)該被“實(shí)例化”。只有在
member functions被使用的時(shí)候,C++ Standard才要求它們被“實(shí)例化”。目前的編譯器并不精確
遵循這項(xiàng)要求。之所以由使用者來主導(dǎo)“實(shí)例化”(instantiation)規(guī)則,有兩個(gè)主要原因:
????? 1)空間和時(shí)間效率的考慮。如果class中有100個(gè)member functions,但程序里只針對(duì)某個(gè)
類型使用其中兩個(gè),針對(duì)另一個(gè)類型使用其中五個(gè),那么將其他193個(gè)函數(shù)都“實(shí)例化”將花費(fèi)大
量的時(shí)間和空間。
????? 2)尚未實(shí)現(xiàn)的機(jī)能。并不是一個(gè)template實(shí)例化的所有類型就一定能夠完整支持一組
member functions所需要的所有運(yùn)算符。如果只“實(shí)例化”那些真正用到的member
functions,template就能夠支持那些原本可能會(huì)造成編譯時(shí)期錯(cuò)誤的類型(types)。
? ? ? 舉個(gè)例子,origin的定義需要調(diào)用Point的default constructor和destructor,那么只有這兩個(gè)
函數(shù)需要被“實(shí)例化”。類似的道理,當(dāng)寫下:
Point <float> *p = new Point <float>;? ? ?? 時(shí),只有(1)Point template的float實(shí)例、(2)new運(yùn)算符、(3)default constructor需要
被“實(shí)例化”。有趣的是,雖然new運(yùn)算符是這個(gè)class的一個(gè)implicitly static member,以至于它
不能夠處理其中任何一個(gè)nonstatic members,但它還是依賴真正的template參數(shù)類型,因?yàn)樗?/p>
的第一參數(shù)size_t代表class的大小。 ??
?????? 這些函數(shù)在什么時(shí)候“實(shí)例化”?目前流行兩種策略:
?????? 1)在編譯的時(shí)候,那么函數(shù)將“實(shí)例化”于origin和p存在的那個(gè)文件中。
?????? 2)在鏈接的時(shí)候。那么編譯器會(huì)被一些輔助工具重新激活。template函數(shù)實(shí)例可能被放在
這一文件中、別的文件中或一個(gè)分離的存儲(chǔ)位置。
?????? 在“int和long一致”(或“double和long double一致”)的架構(gòu)之中,兩個(gè)類型實(shí)例化操作:
Point <int> pi; Point <long> pl;?????? 應(yīng)該產(chǎn)生一個(gè)還是兩個(gè)實(shí)例呢?目前所知道的所有編譯器都產(chǎn)生兩個(gè)實(shí)例(可能有兩組
完整的member functions)。C++ Standard并未對(duì)此有什么強(qiáng)制規(guī)定。
#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; }? ? 當(dāng)輸出“ptr->_x”時(shí):
?
???? 可以看到,指針ptr的確未產(chǎn)生對(duì)象。
???? 當(dāng)輸出“ref._x”時(shí):
????? 可以看到引用產(chǎn)生了對(duì)象實(shí)例。pi和pl是不同的實(shí)例。
在匯編生成的代碼中有:
_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()???? ?? 可以看到的確沒有產(chǎn)生y()函數(shù)和operator delete( void* pointee )函數(shù)的代碼,也產(chǎn)生了int
和long的兩組完整的member functions。
?? 2、Template的錯(cuò)誤報(bào)告(Error Reporting within a Template)
????? 考慮下面的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) }?????? 這個(gè)Mumble template class的聲明內(nèi)含一些既露骨又潛沉的錯(cuò)誤:
????? 1)L4:使用$4字符是不對(duì)的。這項(xiàng)錯(cuò)誤有兩方面。第一,$并不是一個(gè)可以合法用于標(biāo)識(shí)
符的字符;第二,class聲明中只允許有public、protected、private三個(gè)標(biāo)簽(labels),$的出現(xiàn)
使public$不成為public。第一點(diǎn)是語匯(lexical)上的錯(cuò)誤,第二點(diǎn)則是造句/解析(syntactic
/parsing)上的錯(cuò)誤。
?????? 2)L5:t被初始化為整數(shù)常量1024,或許可以,也或許不可以,視T的真實(shí)類型而定。一般
而言,只有template的各個(gè)實(shí)例才診斷得出來。
?????? 3)L6:_t并不是哪一個(gè)member的名稱,tt才是。這種錯(cuò)誤一般會(huì)在“類型檢驗(yàn)”這個(gè)階段被
找出來。是的,每一個(gè)名稱必須綁定于一個(gè)定義身上,要不就會(huì)產(chǎn)生錯(cuò)誤。
?????? 4)L8:!=運(yùn)算符可能已定義好,但也可能還沒有,視T的真正類型而定。和第二點(diǎn)一
樣,只有template的各個(gè)實(shí)例才診斷得出來。
???? ? 5)L9:我們意外地鍵入ex兩次。這個(gè)錯(cuò)誤會(huì)在編譯時(shí)期的解析(parsing)階段被發(fā)現(xiàn)。
C++語言中一個(gè)合法的句子不允許一個(gè)標(biāo)識(shí)符緊跟在另一個(gè)標(biāo)識(shí)符之后。
?????? 6)L13:我們忘記了一個(gè)分號(hào)作為class聲明的結(jié)束。這項(xiàng)錯(cuò)誤也會(huì)在編譯時(shí)期的語句分析
(parsing)階段被發(fā)現(xiàn)。
????? 在一個(gè)nontemplate class聲明中,這6個(gè)既露骨又潛沉的錯(cuò)誤會(huì)被編譯器挑出來。但
template class卻不同。例如,所有與類型有關(guān)的檢驗(yàn),如果牽涉到template參數(shù),都必須延遲
到真正實(shí)例化操作(instantiation)發(fā)生,才得為之。也就是說,L5和L8的潛在錯(cuò)誤會(huì)在每個(gè)實(shí)
例操作(instantiation)發(fā)生時(shí)被檢查出來并記錄之,其結(jié)果將因不同的實(shí)際類型而不同。于是
結(jié)果:
#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; }????
????? 當(dāng)只修改"public$"這一行時(shí):
public:??
??????? 修改"_t(t)"和"ex"兩行:
int ex;...: tt( t )...throw ex;??????? 通過編譯:
??????? 當(dāng)在main函數(shù)里添加:
Mumble<int> mi;則L5和L8是正確的,編譯通過:
?????? 而如果:
Mumble<int*> pmi;??????
?????? 那么L8正確L5錯(cuò)誤,因?yàn)椴荒軌驅(qū)⒁粋€(gè)整數(shù)常量(除了0)指定給一個(gè)指針。
?????? 面對(duì)這樣的聲明:
class SmallInt {public:SmallInt( int _x ) : x( _x ) { }// ...private:int x; };??????? 由于其!=運(yùn)算并未定義,所以下面的句子:
Mumble<SmallInt> smi;??
??????? 會(huì)造成L8錯(cuò)誤,而L5正確。當(dāng)然,下面這個(gè)例子:
Mumble<SmallInt*> psmi;?????? 又造成L8正確而L5錯(cuò)誤。
?????? 那么,什么樣的錯(cuò)誤會(huì)在編譯器處理template聲明時(shí)被標(biāo)示出來?這有一部分和template的
處理策略有關(guān)。cfront對(duì)template的處理完全解析(parse)但不做類型檢驗(yàn);只有在每一個(gè)實(shí)例
化操作(instantiation)發(fā)生時(shí)才做類型檢驗(yàn)。所以在一個(gè)parsing策略之下,所有語匯
(lexing)錯(cuò)誤和解析(parsing)錯(cuò)誤都會(huì)在處理template聲明的過程中被標(biāo)示出來。
??????? 語匯分析器(lexical analyzer)會(huì)在L4捕捉到一個(gè)不合法的字符,解析器(parser)會(huì)這
樣標(biāo)示它:
public$: // caught??????? 表示這是一個(gè)不合法的標(biāo)簽(label)。解析器(parser)不會(huì)把“對(duì)一個(gè)未命名的member
作出參考操作”視為錯(cuò)誤:
_t( t ) // not caught??????? 但它會(huì)抓出L9“ex出現(xiàn)兩次”以及L13“缺少一個(gè)分號(hào)”這兩種錯(cuò)誤。
??????? 在一個(gè)十分普遍的代替策略中,template的聲明被收集成一系列的“l(fā)exical tokens”,而
parsing操作延遲直到真正有實(shí)例化操作(instantiation)發(fā)生時(shí)才開始。每當(dāng)看到一個(gè)
instantiation發(fā)生,這組token就被推往parser,然后調(diào)用類型檢驗(yàn),等等。面對(duì)先前出現(xiàn)的那個(gè)
template聲明,“l(fā)exical tokenizing”會(huì)指出什么錯(cuò)誤嗎?事實(shí)上很少,只有L4所使用的不合法字
符會(huì)被指出。其余的template聲明都被解析為合法的tokens并被收集起來。
??????? 目前的編譯器,面對(duì)一個(gè)template聲明,在它被一組實(shí)際參數(shù)實(shí)例化之前,只能施行以有
限的錯(cuò)誤檢查。template中那些與語法無關(guān)的錯(cuò)誤,程序員可能認(rèn)為十分明顯,編譯器卻通過
了,只有在特定實(shí)例被定義之后,才會(huì)發(fā)出抱怨。這是目前實(shí)現(xiàn)技術(shù)上的一個(gè)大問題。
??????? Nonmember和member template functions在實(shí)例化行為(instantiation)發(fā)生之前也一樣
沒有做完完全的類型檢驗(yàn)。這導(dǎo)致某些十分露骨的template錯(cuò)誤聲明竟然得以通過編譯。例如
下面的template聲明:
template<class type> class Foo {public:Foo();type val();void val( type v );private:type _val; }; // bogus_member不是class的一個(gè)member function // dbx不是class的一個(gè)data member template <class type> double Foo<type>::bogus_member() { return this->dbx; }?????? 在g++4.8.4中,編譯結(jié)果如下:
?????? 可以看到class中的函數(shù)被顯示出錯(cuò)誤。
??????? 如果在class中加入成員函數(shù):
template<class type> class Foo {public:Foo();type val();void val( type v );double bogus_member();private:type _val; };????????? 編譯通過,并不會(huì)報(bào)沒有dbx的錯(cuò)誤。
????????? 這些都是編譯器設(shè)計(jì)者自己的決定。Template facility并沒有說不允許對(duì)template聲明的類
型部分有更嚴(yán)格的檢驗(yàn)。
?????? 3、Template 中的名稱決議法(Name Resolution within a Template)
?????? 必須能夠區(qū)分以下兩種意義。一種是C++ Standard所謂的“scope of the template
definition”,也就是“定義出template”的程序端。另一種是C++ Standard所謂的“scope of the
template instantiation”,也就是“實(shí)例化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中有兩個(gè)foo()調(diào)用操作。在“scope of template definition”中,只有
一個(gè)foo()函數(shù)聲明位于scope之內(nèi)。然而在“scope of template instantiation”中,兩個(gè)foo()函數(shù)聲
明都位于scope之內(nèi)。如果我們有一個(gè)函數(shù)調(diào)用操作:
// scope of the template instantiation sr0.invariant();??????? 那么,在invariant()中調(diào)用的究竟是哪一個(gè)foo()函數(shù)實(shí)例呢?
// 調(diào)用的是哪一個(gè)foo()函數(shù)實(shí)例? _member = foo( _val );??????? 在調(diào)用操作的那一點(diǎn)上,程序中的兩個(gè)函數(shù)實(shí)例是:
// scope of the template declaration extern double foo( double );// scope of the template instantiation extern int foo( int );??????? 而_val的類型是int。結(jié)果被選中的是直覺以外的那一個(gè):
// scope of the template declaration extern double foo ( double );??????
?????? Template之中,對(duì)于一個(gè)nonmember name的決議結(jié)果,是根據(jù)這個(gè)name的使用是否與“用
以實(shí)例化該template的參數(shù)類型”有關(guān)而決定的。如果其使用互不相關(guān),那么就以“scope of the
template declaration”來決定name。如果其使用互有關(guān)聯(lián),那么就以“scope of the template
instantiation”來決定name。在第一個(gè)例子中,foo()與用以實(shí)例化ScopeRules的參數(shù)類型無關(guān):
// the resolution of foo() is not // dependent on the template argument _member = foo( _val );??????? 這是因?yàn)開val的類型是int:_val是一個(gè)“類型不會(huì)變動(dòng)”的template class member。也就是
說,被用來實(shí)例化這個(gè)template的真正類型,對(duì)于_val的類型并沒有影響。此外,函數(shù)的決議結(jié)
果只和函數(shù)的原型(signature)有關(guān),和函數(shù)的返回值沒有關(guān)系。因此_member的類型并不會(huì)
影響哪一個(gè)foo()實(shí)例被選中。foo()的調(diào)用與template參數(shù)毫無關(guān)系!所以調(diào)用操作必須根據(jù)
“scope of the template declaration”來決議。在此scope中,只有一個(gè)foo()候選者(注意,這種
行為不能夠以一個(gè)簡(jiǎn)單的宏擴(kuò)展——像是使用一個(gè)#define宏——重現(xiàn)之)。
?????? 讓我們另外看看“與類型相關(guān)”(type-dependent)的用法:
sr0.type_dependent();?????? 這個(gè)函數(shù)的內(nèi)容如下:
return foo( _member );?????? 這個(gè)例子很清楚地與template參數(shù)有關(guān),因?yàn)樵搮?shù)將決定_member的真正類型。所以這一
次foo()必須在“scope of the template instantiation”中決議,本例中這個(gè)scope有兩個(gè)foo()函數(shù)聲
明。由于_member的類型在本例中為int,所以應(yīng)該是int版的foo()。如果ScopeRules以double
類型實(shí)例化,那么就應(yīng)該是double版的foo()出現(xiàn)。如果ScopeRules是以double類型實(shí)例化,那
么該調(diào)用操作就曖昧不明。最后,如果ScopeRules以某一個(gè)class類型實(shí)例化,而該class沒有
針對(duì)int或double實(shí)現(xiàn)出convertion運(yùn)算符,那么foo()調(diào)用操作會(huì)被表示為錯(cuò)誤。不管如何演變,
都是由“scope of the template instantiation”來決定,而不是由“scope of the template
declaration”。
?????? 這意味著一個(gè)編譯器bib保持兩個(gè)scope contexts:
????? 1)“scope of the template declarartion”,用以專注于一般的template class。
????? 2)“scope of the template instantiation”,用以專注于特定的實(shí)例。
????? 編譯器的決議(resolution)算法必須決定哪一個(gè)才是適當(dāng)?shù)膕cope,然后在其中搜索適當(dāng)?shù)?/p>
name。
?????? 4、Member Function的實(shí)例化行為(Member Funciton Instantiation)
?????? 對(duì)于template的支持,最困難的莫過于template function的實(shí)例化(instantiation)。目前編
譯器提供了兩個(gè)策略:一個(gè)是編譯時(shí)期策略,程序代碼在program text file中備妥可用:另一個(gè)
是鏈接時(shí)期策略,有一些meta-compilation工具可以導(dǎo)引編譯器的實(shí)例化行為
(instantiation)。
??????? 下面是編譯器設(shè)計(jì)者必須回答的三個(gè)主要問題:
??????? 1)編譯器如何找出函數(shù)的定義?
???????? 答案之一是包含template program text file,就好像它是一個(gè)header文件一樣。Borland編
譯器就遵循這個(gè)策略。另一種方法是要求一個(gè)文件命名規(guī)則,例如,我們可以要求,在Point.h
文件中發(fā)現(xiàn)的函數(shù)聲明,其template program text一定要放置于文件Point.C或Point.cpp中,依
此類推。cfront就遵循這個(gè)策略。Edison Design Group編譯器對(duì)這兩種策略都支持。
???????? 2)編譯器如何能夠只實(shí)例化程序中用到的member functions?
???????? 解決辦法之一就是,根本忽略這項(xiàng)要求,把一個(gè)已經(jīng)實(shí)例化的class的所有member
functions都產(chǎn)生出來。Borland就是這么做的——雖然它也提供#pragmas可以壓制(或?qū)嵗?/p>
化)特定實(shí)例。另一種策略就是模擬鏈接操作,檢測(cè)看看哪一個(gè)函數(shù)真正需要,然后只為它
(們)產(chǎn)生實(shí)例。cfront就是這么做的。Edison Design Group編譯器對(duì)這兩種策略都支持。
???????? 3)編譯器如何阻止member definition在多個(gè).o文件中都被實(shí)例化呢?
????????? 解決辦法之一就是產(chǎn)生多個(gè)實(shí)例,然后從鏈接器中提供支持,只留下其中一個(gè)實(shí)例,其余
都忽略。另一個(gè)辦法就是由使用者來引導(dǎo)“模擬鏈接階段”的實(shí)例化策略,決定哪些實(shí)例
(instance)才是所需求的。
????????? 目前,不論是編譯時(shí)期還是鏈接時(shí)期的實(shí)例化(instantiation)策略,均存在以下弱點(diǎn):
當(dāng)template實(shí)例被產(chǎn)生出來時(shí),有時(shí)候會(huì)大量增加編譯時(shí)間。很明顯,這將是template
functions第一次實(shí)例化時(shí)的必要條件。然而當(dāng)那些函數(shù)被非必要地再次實(shí)例化,或是當(dāng)“決定那
些函數(shù)是否需要再實(shí)例化”所花的代價(jià)太大時(shí),編譯器的表現(xiàn)令人失望!
??????? C++支持template的原始意圖可以想見是一個(gè)由使用者導(dǎo)引的自動(dòng)實(shí)例化機(jī)制(use-
directed automatic instantiation mechanism),既不需要使用者的介入,也不需要相同文件有
多次的實(shí)例化行為。但是這已被證明是非常難以達(dá)成的任務(wù),比任何人此刻所能想象的還要
難。ptlink,隨著cfront3.0版所附的原始實(shí)例化工具,提供了一個(gè)由使用者驅(qū)動(dòng)的自動(dòng)實(shí)例化機(jī)
制(use-driven automatic instantiation mechanism),但是它實(shí)在太復(fù)雜了,即使是久經(jīng)世故
的人也沒辦法一下子了解。
???????? Edison Design Group開發(fā)出一套第二代的directed-instantiation機(jī)制,非常接近于
template facility原始含義。它主要運(yùn)作如下:
??????? 1)一個(gè)程序的原始碼被編譯時(shí),最初并不會(huì)產(chǎn)生任何“template實(shí)例化”。然而,相關(guān)信息
已經(jīng)被產(chǎn)生于object files之中。
??????? 2)當(dāng)object files被鏈接在一塊時(shí),會(huì)有一個(gè)prelinker程序被執(zhí)行起來。它會(huì)檢查object
files,尋找template實(shí)例的相互參考以及對(duì)應(yīng)的定義。
??????? 3)對(duì)于每一個(gè)“參考到template實(shí)例”而“該實(shí)例卻沒有定義”的情況,prelinker將該文件視為
與另一個(gè)實(shí)例化(在其中,實(shí)例已經(jīng)實(shí)例化)等同。以這種方法,就可以將必要的程序?qū)嵗?/p>
操作指定給特定的文件。這些都會(huì)注冊(cè)在prelinker所產(chǎn)生的.ii文件中(放在磁盤目錄ii_file)。
??????? 4)prelinker重新執(zhí)行編譯器,重新編譯每一個(gè)“.ii文件曾被改變過”的文件。這個(gè)過程不斷
重復(fù),直到所有必要的實(shí)例化操作都已完成。
??????? 5)所有的object files被鏈接成一個(gè)可執(zhí)行文件。
??????? 這種direct-instantiation體制的主要成本在于,程序第一次被編譯時(shí)的.ii文件設(shè)定時(shí)間。次
要成本則是必須針對(duì)每一個(gè)“complie afterwards”執(zhí)行prelinker,以確保所有被參考到的
templates都存在著定義。在最初的設(shè)計(jì)以及成功地第一次鏈接之后,重新編譯操作包含以下程
序:
????? 1)對(duì)于每一個(gè)將被重新編譯的program text file,編譯器檢查其對(duì)應(yīng)的.ii文件。
????? 2)如果對(duì)應(yīng)的.ii文件列出一組要被實(shí)例化(instantiated)的templates,那些templates(而
且只有那些templates)會(huì)在此編譯時(shí)被實(shí)例化。
?????? 3)prelinker必須執(zhí)行起來,確保所有被參考到的template已經(jīng)被定義妥當(dāng)。
?????? 出現(xiàn)某種形式的automated template機(jī)制,是“對(duì)程序員友善的C++編譯系統(tǒng)”的一個(gè)必要組
件。
?????? 不幸的是,沒有任何一個(gè)機(jī)制是沒有bugs的。Edison Design Group的編譯器使用了一個(gè)由
cfront2.0引入的算法,針對(duì)程序中的每一個(gè)class自動(dòng)產(chǎn)生virtual table的單一實(shí)例。例如下面的
class聲明:
class PrimitiveObject : public Geometry {public:virtual ~PrimitiveObject();virtual void draw();... };?????? 如果它被含入于15個(gè)或45個(gè)程序源碼中,編譯器如何能夠確保只有一個(gè)virtual table實(shí)例被
產(chǎn)生出來呢?產(chǎn)生15份或45份實(shí)例倒還容易些!
?????? Koenig以下面的方法解決這個(gè)問題:每一個(gè)virtual function的地址都被放置于active classes
的virtual table中。如果取得函數(shù)地址,表示virtual function的定義必定出現(xiàn)在程序的某個(gè)地點(diǎn);
否則程序就無法鏈接成功。此外,此函數(shù)只能有一個(gè)實(shí)例,否則也是鏈接不成功。那么,就把
virtual table放在定義了該class之第一個(gè)non-inline、nonpure virtual function的文件中。以我們
的例子而言,編譯器會(huì)將virtual table產(chǎn)生在存儲(chǔ)著virtual destructor的文件之中。
??????? 不幸的是,在template之中,這種單一定義并不一定為真。在template所支持的“將模塊中
的每一樣?xùn)|西都編譯”的模型下,不只是多個(gè)定義可能被產(chǎn)生,而且鏈接器也放任讓多個(gè)定義同
時(shí)出現(xiàn),它只要選擇其中一個(gè)而將其余都忽略,也就是了。
???????? 但Edison Design Group的automatic instantiation機(jī)制做什么事呢?考慮下面這個(gè)library函
數(shù):
void foo( const Point<float> *ptr ) {ptr->virtual_func(); }?????? virtual function call被轉(zhuǎn)換為類似這樣的東西:
// C++偽碼 // ptr->virtual_func(); ( *ptr->_vtbl_Point<float>[ 2 ] )( ptr );??????? 于是導(dǎo)致實(shí)例化(instantiated)Point class的一個(gè)float實(shí)例及其virtual_func()。由于每一個(gè)
virtual function的地址被放置于table之中,如果virtual table被產(chǎn)生出來,每一個(gè)virtual function
也都必須被實(shí)例化(instantiated)。這就是為什么C++ Standard有下面的文字說明的緣故:
?????? 如果一個(gè)vitual function被實(shí)例化(instantiated),其實(shí)例化點(diǎn)緊跟在其class的實(shí)例化點(diǎn)之
后。
?????? 然而,如果編譯器遵循cfront的virtual table實(shí)現(xiàn)體制,那么在”Point的float實(shí)例有一個(gè)virtual
destructor定義被實(shí)例化“之前,這個(gè)table不會(huì)被產(chǎn)生。除非,在這一點(diǎn)上,并沒有顯式使用
virtual destructor以擔(dān)保其實(shí)例化行為(instantiation)。
?????? Edison Design Group的automatic template機(jī)制并不明白它自己的編譯器對(duì)第一個(gè)non-
inline、nonpure virtual function的隱式使用,所以并沒有把它標(biāo)于.ii文件中。結(jié)果,鏈接器反而
回頭抱怨下面這個(gè)符號(hào)沒有出現(xiàn):
_vtbl_Point<float>?????? 并拒絕產(chǎn)生一個(gè)可執(zhí)行文件。Automatic instantiation在此失效!程序員必須顯式地強(qiáng)迫將
destructor實(shí)例化。目前的編譯系統(tǒng)以#program指令來支持此需求。然而C++ Standard也已經(jīng)
擴(kuò)充了對(duì)template的支持,允許程序員顯式地要求在一個(gè)文件中將整個(gè)class template實(shí)例化:
template class Point3d<float>;?????? 或是針對(duì)一個(gè)template class的個(gè)別member function:
template float Point3d<float>::X() const;?????? 或是針對(duì)一個(gè)個(gè)別template function:
template Point3d<float> operator+ ( const Point3d<float>&, const Point3d<float>& );??????? 實(shí)際上,template instantitation似乎拒絕了全面的自動(dòng)化。甚至雖然每一件工作都做對(duì)了,
產(chǎn)生出來的object files的重新編譯成本仍然可能很高——如果程序十分巨大的話!以手動(dòng)方式先
在個(gè)別的object module中完成預(yù)先實(shí)例化操作(pre-instantiation),雖然沉悶,卻是唯一有效
率的做法。
二、異常處理(Exception Handling)
?????? 欲支持exception handling,編譯器的主要工作就是找出catch子句,以處理被拋
出來的exception。這多少需要追蹤程序堆棧中的每一個(gè)函數(shù)的目前作用區(qū)域(包括
追蹤函數(shù)中l(wèi)ocal class objects當(dāng)時(shí)的情況)。同時(shí),編譯器必須提供某種查詢
exception objects的方法,以知道其實(shí)際類型(這直接導(dǎo)致某種形式的執(zhí)行期類型識(shí)
別,也就是RTTI)。最后,還需要某種機(jī)制用以管理被拋出的object,包括它的產(chǎn)
生、存儲(chǔ)、可能的析構(gòu)(如果有相關(guān)的destructor)、清理(clean up)以及一般存
取。也可能有一個(gè)以上的objects同時(shí)起作用。一般而言,exception handling機(jī)制需
要與編譯器所產(chǎn)生的數(shù)據(jù)結(jié)構(gòu)以及執(zhí)行期的一個(gè)exception library緊密合作。在程序
大小和執(zhí)行速度之間,編譯器必須有所抉擇:
????? 1)為了維護(hù)執(zhí)行速度,編譯器可以在編譯時(shí)期建立起用于支持的數(shù)據(jù)結(jié)構(gòu)。這
會(huì)使程序的大小發(fā)生膨脹,但編譯器可以幾乎忽略這些結(jié)構(gòu),直到exception被拋
出。
??????? 2)為了維護(hù)程序大小,編譯器可以在執(zhí)行期建立起用于支持的數(shù)據(jù)結(jié)構(gòu)。這會(huì)影響程序的
執(zhí)行速度,但意味著編譯器只有在必要的時(shí)候才建立那些數(shù)據(jù)結(jié)構(gòu)(并且可以拋棄之)。
??? 1、Exception Handling快速檢閱
??? C++的exception handling由三個(gè)主要的語匯組件構(gòu)成:
??? 1)一個(gè)throw子句。它在程序某處發(fā)出一個(gè)exception。被拋出去的exception可以是內(nèi)建類
型,也可以是使用者自定類型。
??? 2)一個(gè)或多個(gè)catch子句。每一個(gè)catch子句都是一個(gè)exception handler。它用來表示說,這
個(gè)子句準(zhǔn)備處理某種類型的exception,并且在封閉的大括號(hào)區(qū)段中提供實(shí)際的處理程序。
??? 3)一個(gè)try區(qū)段。它被圍繞以一系列的敘述句(statements),這些敘述句可能會(huì)引發(fā)catch
子句起作用。
????? 當(dāng)一個(gè)exception被拋出去時(shí),控制權(quán)會(huì)從函數(shù)調(diào)用中被釋放出來,并尋找一個(gè)吻合的catch
子句。如果沒有吻合者,那么默認(rèn)的處理例程terminate()會(huì)被調(diào)用。當(dāng)控制權(quán)被放棄后,堆棧中
的每一個(gè)函數(shù)調(diào)用也就被推離(popped up)。這個(gè)程序稱為unwinding the stack。在每一個(gè)函
數(shù)被推離堆棧之前,函數(shù)的local class objects的destructor會(huì)被調(diào)用。
????? Exception handling 中比較不那么直覺的就是它對(duì)于那些似乎沒什么事做的函數(shù)所帶來的沖
擊。例如下面這個(gè)函數(shù):
(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) }?????? 如果有一個(gè)exception在第一次調(diào)用foo()(L5)時(shí)被拋出,那么這個(gè)mumble()函數(shù)會(huì)被推出程
序堆棧。由于調(diào)用foo()的操作并不在一個(gè)try區(qū)段之內(nèi),也就不需要嘗試和一個(gè)catch子句吻合。
這里也沒有任何local class objects需要析構(gòu)。然而如果有一個(gè)exception在第二次調(diào)用foo()
(L11)時(shí)被拋出,exception handling機(jī)制就必須在”從程序堆棧中”unwindling“這個(gè)函數(shù)“之
前,先調(diào)用p的destructor。
????? 在exception handling之下,L4~L8和L9~L16被視為兩塊語意不同的區(qū)域,因?yàn)楫?dāng)exception
被拋出來時(shí),這兩塊區(qū)域有不同的執(zhí)行期語意。而且,欲支持exception handling,需要額外的
一些”薄記“操作與數(shù)據(jù)。編譯器的做法有兩種:一種是把兩塊區(qū)域以個(gè)別的”將被摧毀之local
objects“鏈表(已在編譯時(shí)期設(shè)妥)聯(lián)合起來;另一種做法是讓兩塊區(qū)域共享同一個(gè)鏈表,該鏈表
會(huì)在執(zhí)行期擴(kuò)大或縮小。
????? 在程序員層面,exception handling也改變了函數(shù)在資源管理上的語意。例如,下面的函數(shù)
中含有對(duì)一塊共享內(nèi)存的locking和unlocking操作,雖然看起來和exceptions沒有什么關(guān)系,但
在exception handling之下并不保證能夠正確允許:
void mumble( void *arena ) {Point *p = new Point;smLock( arena ); // function call// 如果有一個(gè)exception在此發(fā)生,問題就來了// ...smUnLock( arena ); // function calldelete p; }?????? 本例之中,exception handling機(jī)制把整個(gè)函數(shù)視為單一區(qū)域,不需要操心”將函數(shù)從程序堆
棧中“unwinding”的事情。然而從語意上來說,在函數(shù)被推出堆棧之前,我們需要unlock共享內(nèi)
存,并delete p。讓函數(shù)稱為“exception proof”的最明確(但不是最有效率)方法就是安插一個(gè)
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; }???? 這個(gè)函數(shù)現(xiàn)在有了兩個(gè)區(qū)域:
??? 1)try block以外的區(qū)域,在那里,exception handling機(jī)制除了“pop”程序堆棧之外,沒有其
他事情要做。
??? 2)try block以內(nèi)的區(qū)域(以及它所聯(lián)合的default catch子句)。
??? 請(qǐng)注意,new運(yùn)算符的調(diào)用并非在try區(qū)段內(nèi)。如果new運(yùn)算符或是Point constructor在配置內(nèi)
存之后發(fā)生一個(gè)exception,那么內(nèi)存既不會(huì)被unlocking,p也不會(huì)被delete(這兩個(gè)操作都在
catch區(qū)段內(nèi))。這是正確的語意嗎?
??? 是的,它是。如果new運(yùn)算符拋出一個(gè)exception,那么就不需要配置heap中的內(nèi)存,Point
constructor也不需要被調(diào)用。所以也就沒有理由調(diào)用delete運(yùn)算符。然而如果是在Point
constructor中發(fā)生exception,此時(shí)內(nèi)存已配置完成,那么Point之中任何構(gòu)建好的合成物或子對(duì)
象(subobject,也就是一個(gè)member class object或base class object)都將自動(dòng)被析構(gòu)掉,然
后heap內(nèi)存也會(huì)被釋放掉。不論哪種情況,都不需要delete運(yùn)算符。
???? 類似的道理,如果一個(gè)exception是在new運(yùn)算符執(zhí)行過程中被拋出的,arena所指向的內(nèi)存
就絕不會(huì)被locked,因此,也沒有必要unlock之。
???? 處理這些資源管理問題,一個(gè)建議辦法就是,將資源需求封裝于一個(gè)class object體內(nèi),并由
destructor來釋放資源(然而如果資源必須被索求、被釋放、再被索求、再被釋放......許多次的
時(shí)候,這種風(fēng)格會(huì)變得優(yōu)點(diǎn)累贅):
void mumble( void *arena ) {auto_ptr<Point> ph ( new Point );SMLock sm( arena );// 如果這里拋出一個(gè)exception,現(xiàn)在就沒有問題了// ...// 不需要顯式地unlock和delete// local destructors在這里被調(diào)用// sm.SMLock::~SMLock();// ph.auto_ptr<Point>::~auto_ptr<Point>() }?????? 從exception handling的角度看,這個(gè)函數(shù)現(xiàn)在有三個(gè)區(qū)段:
????? 1)第一區(qū)是auto_ptr被定義之處。
????? 2)第二區(qū)段是SMLock被定義之處。
????? 3)上述兩個(gè)定義之后的整個(gè)函數(shù)。
?????? 如果exception是在auto_ptr constructor中被拋出的,那么就沒有active local objects需要被
EH機(jī)制摧毀。然而如果SMLock constructor中拋出一個(gè)exception,auto_ptr object必須在
“unwinding”之前先被摧毀。至于在第三個(gè)區(qū)段中,兩個(gè)local objects當(dāng)然都必須被摧毀。
??????? 支持EH,會(huì)使那些擁有member class subobjects或base class subobjects(并且它們也都
有constructors)的classes的constructor更復(fù)雜。一個(gè)class如果被部分構(gòu)造,其destructor必須
只施行于那些已被構(gòu)造的subobjets和(或)member objects身上。例如,假設(shè)class X有
member objects A, B和C,都各有一對(duì)constructor和destructor,如果A的constructor拋出一個(gè)
exception,不論A、B或C都不需要調(diào)用其destructor。如果B的constructor拋出一個(gè)
exception,A的destructor必須被調(diào)用,但C不用。處理所有這些意外事故,是編譯器的責(zé)任。
?????? 同樣的道理,如果程序員寫下:
// class Point3d : public Point2d { ... } Point3d *cvs = new Point3d[ 512 ];? ? ?? 會(huì)發(fā)生兩件事:
? ? ?? 1)從heap中配置足以給512個(gè)Point3d objects所用的內(nèi)存。
? ? ?? 2)如果成功,先是Point2d constructor,然后是Point3d constructor,會(huì)施行于每一個(gè)元素
身上。 ?
?????? 如果#27元素的Point3d constructor拋出一個(gè)exception,會(huì)怎樣?對(duì)于#27元素,只有
Point2d destructor需要調(diào)用執(zhí)行。對(duì)于前26個(gè)元素,Point3d destructor和Point2d destructor都
需要調(diào)用執(zhí)行。然后內(nèi)存必須被釋放回去。
??? 2、對(duì)Exception Handling的支持
? ?? 當(dāng)一個(gè)exception發(fā)生時(shí),編譯系統(tǒng)必須完成以下事情:
? ? 1)檢驗(yàn)發(fā)生throw操作的函數(shù)。
? ? 2)決定throw操作是否發(fā)生在try區(qū)段中。
? ? 3)若是,編譯系統(tǒng)必須把exception type拿來和每一個(gè)catch子句進(jìn)行比較。
? ? 4)如果比較后吻合,流程控制應(yīng)該交到catch子句手中。
? ? 5)如果throw的發(fā)生并不在try區(qū)段中,或沒有一個(gè)catch子句吻合,那么系統(tǒng)必須(a)摧毀
所有active local objects,(b)從堆棧中將目前的函數(shù)“unwind”掉,(c)進(jìn)行到程序堆棧的下
一個(gè)函數(shù)中去,然后重復(fù)上述步驟2~5。?
???? 決定throw是否發(fā)生在一個(gè)try區(qū)段中
???? 一個(gè)函數(shù)可以被想象為好幾個(gè)區(qū)域:
??? 1)try區(qū)段以外的區(qū)域,而且沒有active local objects。
??? 2)try區(qū)段以外的區(qū)域,但有一個(gè)(或以上)的active local objects需要析構(gòu)。
??? 3)try區(qū)段以內(nèi)的區(qū)域。
???? 編譯器必須表示出以上各區(qū)域,并使它們對(duì)執(zhí)行期的exception handling系統(tǒng)有所作用。一個(gè)
很棒的策略就是構(gòu)造出program counter-range表格。
????? program counter(EIP寄存器)內(nèi)含下一個(gè)即將執(zhí)行的程序指令。為了在一個(gè)內(nèi)含try區(qū)段的
函數(shù)中表示出某個(gè)區(qū)域,可以把program counter的起始值和結(jié)束值(或是起始值和范圍)存儲(chǔ)
在一個(gè)表格中。
????? 當(dāng)throw操作發(fā)生時(shí),目前的program counter值被拿來與對(duì)應(yīng)的“范圍表格”進(jìn)行對(duì)比,比決
定目前作用中的區(qū)域是否在一個(gè)try區(qū)域中。如果是,就需要找出相關(guān)的catch子句。如果這個(gè)
exception無法被處理(或者它被再次拋出),目前的這個(gè)函數(shù)會(huì)從程序中被推出(popped),
而program counter會(huì)被設(shè)定為調(diào)用端地址,然后這樣的循環(huán)再重新開始。
?????? 將exception的類型和每一個(gè)catch子句的類型做比較
?????? 對(duì)于每一個(gè)被拋出來的exception,編譯器必須產(chǎn)生一個(gè)類型描述器,對(duì)exception的類型進(jìn)
行編碼。如果那是一個(gè)derived type,編碼內(nèi)容必須包括其所有base class的類型信息。只編進(jìn)
public base class的類型是不夠的,因?yàn)檫@個(gè)exception可能被一個(gè)member function捕捉,而在
一個(gè)member function的范圍(scope)之中,derived class和nonpublic base class之間可以轉(zhuǎn)
換。
??????? 類型描述器(type descriptor)是必要的,因?yàn)檎嬲膃xception是在執(zhí)行期被處理的,其
object必須有自己的類型信息。RTTI正是因?yàn)橹С諩H而獲得的副產(chǎn)品。
??????? 編譯器還必須為每一個(gè)catch子句產(chǎn)生一個(gè)類型描述器。執(zhí)行期的exception handler會(huì)將“被
拋出之object的類型描述器”和“每一個(gè)cause子句的類型描述器”進(jìn)行比較,直到找到吻合的一
個(gè),或是直到堆棧已經(jīng)被“unwound”而terminate()已被調(diào)用。
?????? 每一個(gè)函數(shù)會(huì)產(chǎn)生一個(gè)exception表格,它描述與函數(shù)相關(guān)的各區(qū)域,任何必要的善后處理
代碼(cleanup code,被local class object destructors調(diào)用)以及catch子句的位置(如果某個(gè)
區(qū)域是在try區(qū)段之中的話)。
?????? 當(dāng)一個(gè)實(shí)際對(duì)象在程序執(zhí)行時(shí)被拋出,會(huì)發(fā)生什么事?
?????? 當(dāng)一個(gè)exception被拋出時(shí),exception object會(huì)被產(chǎn)生出來并通常放置在相同形式的
exception數(shù)據(jù)堆棧中。從throw端傳給catch子句的,是exceotion object的地址、類型描述器
(或是一個(gè)函數(shù)指針,該函數(shù)會(huì)傳回與該exception type有關(guān)的類型描述器對(duì)象)以及可能會(huì)有
的exception object描述器(如果有人定義它的話)。
????? 考慮一個(gè)catch子句如下:
catch ( exPoint p ) {// do somethingthrow; }?????? 以及一個(gè)exception object,類型為exVertex,派生自exPoint。這兩種類型都吻合,于是
catch子句會(huì)作用起來。那么p會(huì)發(fā)生什么事?
?????? 1)p將以exception object作為初值,就像一個(gè)函數(shù)參數(shù)一樣。這意味著如果定義有(或由
編譯器合成出)一個(gè)copy constructor和一個(gè)destructor的話,它們都會(huì)實(shí)施于local copy身上。
?????? 2)由于p是一個(gè)object而不是一個(gè)reference,當(dāng)其內(nèi)容被拷貝的時(shí)候,這個(gè)exception
object的non-exPoint部分會(huì)被切掉(sliced off)。此外,如果為了exception的繼承而提供
virtual function,那么p的vptr會(huì)被設(shè)為exPoint的virtual table;exception object的vptr不會(huì)拷貝。?
? ? ?? 當(dāng)這個(gè)exception被再拋出一次時(shí),會(huì)發(fā)生什么事情?p現(xiàn)在是繁殖出來的object?還是從
throw端產(chǎn)生的原始exception object?p是一個(gè)local object,在catch子句的末端將被摧毀。拋出
p需要產(chǎn)生另一個(gè)臨時(shí)對(duì)象,并意味著喪失了原來的exception的exVertex部分。原來的
exception object被再一次拋出:任何對(duì)p的修改都會(huì)被拋棄。 ????
??????? 像下面這樣的一個(gè)catch子句:
catch( exPoint &rp ) {// do somethingthrow; }??????? 則是參考到真正的exception object。任何虛擬調(diào)用都會(huì)被決議(resolved)為instances
active for exVertex,也就是exception object的真正類型。任何對(duì)此object的改變都被繁殖到下
一個(gè)catch子句。
??????? 最后,這里提出一個(gè)有趣的謎題。如果我們有下面的throw操作:
exVertex errVer;// ... mumble() {// ...if( mumble_cond ){errVer.fileName( "mumble()" );throw errVer;}// ... }????? 究竟是真正的exception errVer被繁殖,還是errVer的一個(gè)復(fù)制品被構(gòu)造于exception stack之
中并不被繁殖?答案是一個(gè)復(fù)制品被構(gòu)造出來,全局性的errVer并沒有被繁殖。這意味著在一個(gè)
catch子句中對(duì)于exception object的任何改變都是局部性的,不會(huì)影響errVer。只是在一個(gè)catch
子句評(píng)估完畢并且知道它不會(huì)再拋出excption之后,真正的exception object才會(huì)被摧毀。
三、執(zhí)行期類型識(shí)別(Runtime Type Identification,RTTI)
???? 在cfront中,用以表現(xiàn)出一個(gè)程序的所謂“內(nèi)部類型體系”,看起來像這樣:
// 程序?qū)哟谓Y(jié)構(gòu)的根類(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 { ... };??? 其中g(shù)en是generic的簡(jiǎn)寫,用來表現(xiàn)一個(gè)overloaded function。
??? 于是只要你有一個(gè)變量,或是類型為type*的成員(并知道它代表一個(gè)函數(shù)),你就必須決定
其特定的derived type是否為fct或是gen。在2.0之前,除了destructor之外唯一不能夠被overload
的函數(shù)就是conversion運(yùn)算符,例如:
class String {public:operator char*();// ... };???? 在2.0導(dǎo)入const member functions之前,conversion運(yùn)算符不能夠被overload,因?yàn)樗鼈儾?/p>
使用參數(shù)。直到引進(jìn)了const member functions,情況才有所變化?,F(xiàn)在,像下面這樣的聲明就
可能了:
class String {public:// ok with Release 2.0operator char*();operator char*() const;// ... };????? 也就是說,在2.0版本之前,以一個(gè)explicit cast來存取derived object總是安全(而且比較快
速)的,像下面這樣:
typedef type *ptype; typedef fct *pfct;simlipy_conv_op( ptype pt ) {// ok : conversion operators can only be fctspfct pf = pfct( pt );// ... }????? 在const member functions引入之前,這份代碼是正確的。但之后就不對(duì)了。是因?yàn)镾tring
class聲明的改變,因?yàn)閏har* conversion運(yùn)算符現(xiàn)在被內(nèi)部視為一個(gè)gen而不是一個(gè)fct。
????? 下面這樣的轉(zhuǎn)換形式:
pfct pf = pfct( pt );????? 被稱為downcast(向下轉(zhuǎn)換),因?yàn)樗行У匕岩粋€(gè)base class轉(zhuǎn)換至繼承架構(gòu)的末端,變
成其derived classes中某一個(gè)Downcast有潛在性的危險(xiǎn),因?yàn)樗糁屏祟愋拖到y(tǒng)的作用,不正
確的使用可能會(huì)帶來錯(cuò)誤的解釋(如果它是一個(gè)read操作)或腐蝕掉程序內(nèi)存(如果它是一個(gè)
write操作)。在我們的例子中,一個(gè)指向gen object的指針被不正確地轉(zhuǎn)換為一個(gè)指向fct object
的指針pf。所有后續(xù)對(duì)pf的使用都是不正確的(除非只是檢查它是否為0,或只是把它拿來和其
他指針進(jìn)行比較)。
???? 1、Type-Safe Downcast(保證安全的向下轉(zhuǎn)換操作)
???? C++被吹毛求疵的一點(diǎn)就是,它缺乏一個(gè)保證安全的downcast(向下轉(zhuǎn)換操作)。只有在“類
型真的可以被適當(dāng)轉(zhuǎn)換”的情況下,才能夠執(zhí)行downcast。一個(gè)type-safe downcast必須在執(zhí)行
期有所查詢,看看它是否指向它所展現(xiàn)(表達(dá))之object的真正類型。因此,欲支持type-safe
downcast,在object空間和執(zhí)行時(shí)間上都需要一些額外負(fù)擔(dān):
??? 1)需要額外的空間以存儲(chǔ)類型信息(type information),通常是一個(gè)指針,指向某個(gè)類型信
息節(jié)點(diǎn)。
??? 2)需要額外的空間以決定執(zhí)行期的類型(runtime type),因?yàn)?#xff0c;正如其名所示,這需要在
執(zhí)行期才能決定。
??? 這樣的機(jī)制面對(duì)下面這樣平常的C結(jié)構(gòu),會(huì)如何影響其大小、效率以及鏈接兼容性呢?
char *winnie_tbl[] = { "rumbly in my tummy", "oh, bother" };??? 它所導(dǎo)致的空間和效率上的不良后果甚為可觀。
???? 沖突發(fā)生在兩組使用者之間:
??? 1)程序員大量使用多態(tài)(polymorphism),并因而需要正統(tǒng)而合法的大量downcast操作。
??? 2)程序員使用內(nèi)建數(shù)據(jù)類型以及非多態(tài)設(shè)備,因而不受各種額外負(fù)擔(dān)所帶來的不良后果。
???? 理想的解決方案是,為兩派使用者提供正統(tǒng)而合法的需要——雖然或許得犧牲一些設(shè)計(jì)上的
純度與優(yōu)雅性。
???? C++的RTTI機(jī)制提供了一個(gè)安全的downcast設(shè)備,但支隊(duì)那些展現(xiàn)”多態(tài)(也就是使用繼承
和動(dòng)態(tài)綁定)“的類型有效。我們?nèi)绾畏直孢@些?編譯器能否光看class 的定義就決定這個(gè)class
用以表現(xiàn)一個(gè)獨(dú)立的ADT還是一個(gè)支持多態(tài)的可繼承子類型(subtype)?當(dāng)然,策略之一就是
導(dǎo)入一個(gè)新的關(guān)鍵詞,優(yōu)點(diǎn)是可以清楚地識(shí)別支持新特性的類型,缺點(diǎn)則是必須翻新舊的程
序。
????? 另一個(gè)策略是通過聲明一個(gè)或多個(gè)virtual functions來區(qū)別class聲明。其優(yōu)點(diǎn)是透明化地將舊
有程序轉(zhuǎn)換過來,只要重新編譯就好。缺點(diǎn)則是可能會(huì)將一個(gè)起始并非必要的virtual function強(qiáng)
迫導(dǎo)入繼承體系的base class身上。這正是目前RTTI機(jī)制所支持的策略。在C++中,一個(gè)具備
多態(tài)性質(zhì)的class(所謂的pilymorphic class),正式內(nèi)含著繼承而來(或直接聲明)的virtual
functions。
????? 從編譯器的角度來說,這個(gè)策略還有其他優(yōu)點(diǎn),就是大量降低額外負(fù)擔(dān)。所有polymorphic
classes的objects都維護(hù)了一個(gè)指針(vptr),指向virtual function table。只要我們把與該class
相關(guān)的RTTI object地址放進(jìn)virtual table(通常是第一個(gè)slot),那么額外負(fù)擔(dān)就降低為:每一
個(gè)class object只多花費(fèi)一個(gè)指針。這一指針只需要被設(shè)定一次,它是被編譯器靜態(tài)設(shè)定的,而
非在執(zhí)行期由class constructor設(shè)定(vptr才是這么設(shè)定的)。
???? 2、Type-Safe Dynamic Cast(保證安全的動(dòng)態(tài)轉(zhuǎn)換)
????? dunamic_cast運(yùn)算符可以在執(zhí)行期決定真正的類型。如果downcast是安全的(也就是說,
如果base type pointer指向一個(gè)derived class object),這個(gè)運(yùn)算符會(huì)傳回適當(dāng)轉(zhuǎn)換過的指針。
如果downcast不是安全的,這個(gè)運(yùn)算符會(huì)傳回0。下面就我們?nèi)绾沃貙懳覀冊(cè)镜腸front
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的一個(gè)類型描述器會(huì)被編譯器產(chǎn)生出來。由pt所指向
的class object類型描述器必須在執(zhí)行期通過vptr取得。下面就是可能的轉(zhuǎn)換:
// 取得pt的類型描述器 ( ( type_info* )( pt->vptr[ 0 ] ) )->_type_descriptor;????? type_info是C++ Standard所定義的類型描述器的class名稱,該class中放置著帶索求的類型
信息。virtual table的第一個(gè)slot內(nèi)含type_info object的地址;此type_info object與pt所指的class
type有關(guān)。這兩個(gè)類型描述器被交給一個(gè)runtime library函數(shù),比較之后告訴我們是否吻合。很
顯然這筆static cast昂貴得多,但卻安全得多(如果我們把一個(gè)fct類型”downcast”為一個(gè)gen類
型的話)。
???? 最初對(duì)runtime cast的支持提議中,并未引進(jìn)任何關(guān)鍵詞或額外的語法。下面這樣的轉(zhuǎn)換操
作:
// 最初對(duì)runtime cast的提議語法 pfct pf = pfct( pt );???? 究竟是static還是dynamic,必須視pt是否指向一個(gè)多態(tài)class object而定。
???? 3、References并不是Pointers
???? 程序執(zhí)行中對(duì)一個(gè)class指針類型施以dynamic_cast運(yùn)算符,會(huì)獲得true或false:
?? 1)如果傳回真正的地址,則表示這一object的動(dòng)態(tài)類型被確認(rèn)了,一些與類型有關(guān)的操作現(xiàn)
在可以施行于其上。
?? 2)如果傳回0,則表示沒有指向任何object,意味著應(yīng)該以另一種邏輯施行于這個(gè)動(dòng)態(tài)類型未
確定的object身上。 ?
??? dynamic_cast運(yùn)算符也使用于reference身上。然而對(duì)于一個(gè)non-type-safe cast,其結(jié)果不會(huì)
與施行于指針的情況相同。為什么?一個(gè)reference設(shè)為0,會(huì)引起一個(gè)臨時(shí)性對(duì)象(擁有被參考
的類型)被產(chǎn)生出來,該臨時(shí)對(duì)象的初值為0,這個(gè)reference然后被設(shè)定成為該臨時(shí)對(duì)象的一個(gè)
別名(alias)。因此當(dāng)dynamic_cast運(yùn)算符施行于一個(gè)reference時(shí),不能夠提供對(duì)等于指針情
況下的那一組true/false。取而代之的是,會(huì)發(fā)生下列事情:
??? 1)如果reference真正參考到適當(dāng)?shù)膁erived class(包括下一層或下下一層,或下下下一層
或...),downcast會(huì)被執(zhí)行而程序可以繼進(jìn)行。
???? 2)如果reference并不真正是某一種derived class,那么,由于不能夠傳回0,因此拋出一個(gè)
bad_cast exception。
???? 下面是重新實(shí)現(xiàn)后的simplify_conv_op函數(shù),參數(shù)改為一個(gè)reference:
simplify_conv_op( const type &rt ) {try{fct &rf = dynamic_cast<fct&>( rt );// ... }catch( bad_cast ){// ... mumble ...} }???? 其中執(zhí)行的操作十分理想地表現(xiàn)出某種exception failure,而不知是簡(jiǎn)單(一如從前)的控制
流程。
???? 4、Typeid運(yùn)算符
???? 使用typeid運(yùn)算符,就有可能以一個(gè)reference達(dá)到相同的執(zhí)行期代替路線(runtime
"alternative pathway"):
simplify_conv_op( const type &rt ) {if( typeid( rt ) == typeid( fct ) ){fct &rf = static_cast<fct&>( rt );// ...}else { ... } }???? 在這里,一個(gè)明顯的較好實(shí)現(xiàn)策略是在gen和fctlasses中都引進(jìn)一個(gè)virtual function。
????? typeid運(yùn)算符傳回一個(gè)const reference,類型為type_info。在先前測(cè)試中出現(xiàn)的equlity(等
號(hào))運(yùn)算符,其實(shí)是一個(gè)被overloaded的函數(shù):
bool type_info:: operator==( const type_info& ) const;????? 如果兩個(gè)type_info objects相等,這個(gè)equality運(yùn)算符就傳回true。
????? type_info object由什么組成?C++ Standard中對(duì)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 copytype_info( const type_info& );type_info& operator=( const type_info& );// data members };?????? 編譯器必須提供的最小量信息是class的真實(shí)名稱和在type_info objects之間的某些排序算符
(這就是before()函數(shù)的目的),以及某些形式的描述器,用來表現(xiàn)eplicit class type和這一
class的任何subtypes。
?????? 雖然RTTI提供的type_info對(duì)于exception handling的支持是必要的,但對(duì)于exception
handling的完整支持而言,還不夠。如果再加上額外的一些type_info derived classes,就可以
在exception發(fā)生時(shí)提供關(guān)于指針、函數(shù)、類等等的更詳細(xì)信息。例如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只適用于多態(tài)類(ploymorphic classese),事實(shí)上type_info
objects也適用于內(nèi)建類型,以及非多態(tài)的使用者自定類型。這對(duì)于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 ) ...;???? 會(huì)傳回一個(gè)const type_info&。這與先前使用多態(tài)類型(polymorphic types)的差異在于,這
時(shí)候的type_info object是靜態(tài)取得,而非執(zhí)行期取得。一般的實(shí)現(xiàn)策略是在需要時(shí)才產(chǎn)生
type_info object,而非程序一開頭就產(chǎn)生之。
四、效率有了,彈性呢?
??? 傳統(tǒng)的C++對(duì)象模型提供有效率的執(zhí)行期支持。這份效率,再加上與C之間的兼容性,造成了
C++的廣泛被接受度。然而,在某些領(lǐng)域方面,像是動(dòng)態(tài)共享函數(shù)庫(dynamically shared
libraries)、共享內(nèi)存(shared memory)以及分布式對(duì)象(distrubuted object)方面,這個(gè)對(duì)
象模型的彈性還是不夠。
?
?
?????
?
?
?
?
?
?
轉(zhuǎn)載于:https://my.oschina.net/u/2537915/blog/713503
總結(jié)
以上是生活随笔為你收集整理的C++对象模型学习——站在对象模型的尖端的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring-AOP实践 - 统计访问时
- 下一篇: 临时起异,要进入C++领域耍一个程序