我們曾經(jīng)在討論C++的時候,經(jīng)常會問到:“虛函數(shù)能被聲明為內(nèi)聯(lián)嗎?”現(xiàn)在,我們幾乎聽不到這個問題了。現(xiàn)在聽到的是:“你不應(yīng)該使print成為內(nèi)聯(lián)的。聲明一個虛函數(shù)為內(nèi)聯(lián)是錯誤的!”
這種說法的兩個主要的原因是(1)虛函數(shù)是在運行期決議而內(nèi)聯(lián)是一個編譯期動作,所以,我們將虛函數(shù)聲明為內(nèi)聯(lián)并得不到什么效果;(2)聲明一個虛函數(shù)為內(nèi)聯(lián)導(dǎo)致了函數(shù)的多分拷貝,而且我們?yōu)橐粋€不應(yīng)該在任何時候內(nèi)聯(lián)的函數(shù)白白花費了存儲空間。這樣做很沒腦子。
www.601456.com 不過,事實并不是這樣。我們先來看看第一個:許多情況下,虛擬函數(shù)都被靜態(tài)地決議了——比如在派生類虛擬函數(shù)中調(diào)用基類的虛擬函數(shù)的時候。為什么這樣做呢?封裝。一個比較明顯的例子就是派生類析構(gòu)函數(shù)調(diào)用鏈。所有的虛析構(gòu)函數(shù),除了最初觸發(fā)這個析構(gòu)鏈的虛析構(gòu)函數(shù),都被靜態(tài)的決議了。如果不將基類的虛析構(gòu)函數(shù)內(nèi)聯(lián),我們無法從中獲利[a]。這和不內(nèi)聯(lián)一個虛擬析構(gòu)函數(shù)有什么不同嗎?如果繼承體系層次比較深并且有許多這樣的類的實例要被銷毀的話,答案是肯定的。
再來看另外一個不用析構(gòu)函數(shù)的例子,想象一下設(shè)計一個圖書館類。我們將MaterialLocation作為抽象類LibraryMaterial的一個成員。將它的print成員函數(shù)聲明為一個純虛函數(shù),并且提供函數(shù)定義:它輸出MaterialLocation。
class LibraryMaterial {
private:
MaterialLocation _loc; // shared data
// ...
public:
// declares pure virtual function
inline virtual void print( ostream& = cout ) = 0;
};
// we actually want to encapsulate the handling of the
// location of the material within a base class
// LibraryMaterial print() method - we just don’t want it
// invoked through the virtual interface. That is, it is
// only to be invoked within a derived class print() method
inline void
LibraryMaterial::
print( ostream &os ) { os <<_loc; }
接著,我們引入一個Book類,它的print函數(shù)輸出Title, Author等等。在這之前,它調(diào)用基類的print函數(shù)(LibraryMaterial::print())來顯示書本位置(MaterialLocation)。如下:
inline void
Book::
www.liuhebao.com print( ostream &os )
{
// ok, this is resolved statically,
// and therefore is inline expanded ...
LibraryMaterial::print();
os <<"title:" <<_title
<< "author" <<_author <
www.szfuao.com AudioBook類,派生于Book類,并加入附加信息,比如旁述,音頻格式等等。這些東西都用它的print函數(shù)輸出。再這之前,我們需要調(diào)用Book::print()來顯示前面的信息。
inline void
AudioBook::
print( ostream &os )
{
// ok, this is resolved statically,
// and therefore is inline expanded ...
Book::print();
www.yzjxsp.com os <<"narrator:" <<_narrator <
}
這和虛析構(gòu)函數(shù)調(diào)用鏈的例子一樣,都只是最初調(diào)用的虛函數(shù)沒有被靜態(tài)決議,其它的都被原地展開。This unnamed hierarchical design pattern is significantly less effective if we never declare a virtual function to be inline.
那么對于第二個原因中代碼膨脹的問題呢?我們來分析一下,如果我們寫下:
LibraryMaterial *p =
new AudioBook( "Mason &Dixon",
"Thomas Pynchon", "Johnny Depp" );
// ...
p->print();
這個print實例是內(nèi)聯(lián)的嗎?不,當(dāng)然不是。這樣不得不通過虛擬機制在運行期決議。這讓print實例放棄了對它的內(nèi)聯(lián)聲明了嗎?也不是。這個調(diào)用轉(zhuǎn)換為下面的形式(偽代碼):
// Pseudo C++Code
www.yzsws.com // Possible transformation of p->print()
( *p->_vptr[ 2 ] )( p );
where 2 represents the location of print within the associated virtual function table.因為調(diào)用print是通過函數(shù)指針_vptr[2]進行的,所以,編譯器不能靜態(tài)的決定這個調(diào)用地址,并且,這個函數(shù)也不能內(nèi)聯(lián)。
當(dāng)然,虛函數(shù)print的內(nèi)聯(lián)實體(definition)也必須在某個地方表現(xiàn)出來。 即是說,至少有一個函數(shù)實體是在virtual table調(diào)用的地址原地展開的。編譯器是如何決定在何時展開這個函數(shù)實體呢?其中一個編譯(implementaion)策略是當(dāng)virtual table生成的同時,生成這個函數(shù)實體。這就是說對于每一個派生類的virtual table都會生成一個函數(shù)實體。
在一個可應(yīng)用的類[b]中有多少vitrual table會被生成呢?呵呵,這是一個好問題。C++標(biāo)準(zhǔn)中對虛函數(shù)行為進行了規(guī)定,但是沒有對函數(shù)實現(xiàn)進行規(guī)定。由于virtual table沒有在C++標(biāo)準(zhǔn)中進行規(guī)定,很明顯,究竟這個virtual table怎樣生成,和究竟要生成多少個vitrual table也沒有規(guī)定。多少個?當(dāng)然,我們只要一個。Stroustrup的cfront編譯器,很巧妙的處理了這些情況.( Stan and Andy Koenig described the algorithm in the March 1990 C++ Report article, "Optimizing Virtual Tables in C++ Release 2.0.")
Moreover, the C++ Standard now requires that inline functions behave as though only one definition for an inline function exists in the program even though the function may be defined in different files。新的規(guī)則要求編譯器只展開一個內(nèi)聯(lián)虛函數(shù)。如果一點被廣泛采用的話,虛函數(shù)的內(nèi)聯(lián)導(dǎo)致的代碼膨脹問題就會消失。
www.yzyedu.com [譯注:C++ Standard: 9.3.8, Member function of local class shall be defined inline in their class defination, if they are defined at all]
============================
譯注:
[a]函數(shù)調(diào)用開銷,調(diào)用基類虛函數(shù)的時候至少要經(jīng)過兩次間接過程(S. B.Lippman: 《Inside the C++ Object Model》)
[b]一個產(chǎn)品類(?)
總結(jié):
就是虛函數(shù)inline在調(diào)用鏈等地方很有用~
即使沒有加入inline聲明,作為一個好編譯器,都會優(yōu)化(虛析構(gòu)函數(shù))
在很長的函數(shù)調(diào)用鏈中,最好將鏈中基類的函數(shù)inline,這樣節(jié)約開銷
至于在什么地方inline,由編譯器決定,因為C++標(biāo)準(zhǔn)沒有規(guī)定
新C++標(biāo)準(zhǔn)(可能沒有通過)中,規(guī)定了,inline化只對產(chǎn)品類有效,且只動作一次
保證代碼不過度膨脹
inline動作是在產(chǎn)品類實例化同時,和vtable生成一起