我們曾經在讨論C++的時候,經常會問到:“虛函數能被聲明為内聯嗎?”現在,我們幾乎聽不到這個問題了。現在聽到的是:“你不應該使print成為内聯的。聲明一個虛函數為内聯是錯誤的!”
這種說法的兩個主要的原因是(1)虛函數是在運作期決議而内聯是一個編譯期動作,是以,我們将虛函數聲明為内聯并得不到什麼效果;(2)聲明一個虛函數為内聯導緻了函數的多分拷貝,而且我們為一個不應該在任何時候内聯的函數白白花費了存儲空間。這樣做很沒腦子。
不過,事實并不是這樣。我們先來看看第一個:許多情況下,虛拟函數都被靜态地決議了——比如在派生類虛拟函數中調用基類的虛拟函數的時候。為什麼這樣做呢?封裝。一個比較明顯的例子就是派生類析構函數調用鍊。所有的虛析構函數,除了最初觸發這個析構鍊的虛析構函數,都被靜态的決議了。如果不将基類的虛析構函數内聯,我們無法從中獲利[a]。這和不内聯一個虛拟析構函數有什麼不同嗎?如果繼承體系層次比較深并且有許多這樣的類的執行個體要被銷毀的話,答案是肯定的。
再來看另外一個不用析構函數的例子,想象一下設計一個圖書館類。我們将MaterialLocation作為抽象類LibraryMaterial的一個成員。将它的print成員函數聲明為一個純虛函數,并且提供函數定義:它輸出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函數輸出Title, Author等等。在這之前,它調用基類的print函數(LibraryMaterial::print())來顯示書本位置(MaterialLocation)。如下:
Book::
{
// ok, this is resolved statically,
// and therefore is inline expanded …
LibraryMaterial::print();
os << "title:" << _title
<< "author" << _author << endl;
}
AudioBook類,派生于Book類,并加入附加資訊,比如旁述,音頻格式等等。這些東西都用它的print函數輸出。再這之前,我們需要調用Book::print()來顯示前面的資訊。
AudioBook::
Book::print();
os <<"narrator:"<< _narrator <<endl;
這和虛析構函數調用鍊的例子一樣,都隻是最初調用的虛函數沒有被靜态決議,其它的都被原地展開。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執行個體是内聯的嗎?不,當然不是。這樣不得不通過虛拟機制在運作期決議。這讓print執行個體放棄了對它的内聯聲明了嗎?也不是。這個調用轉換為下面的形式(僞代碼):
// Pseudo C++ Code
// Possible transformation of p->print()
(*p->_vptr[ 2 ])(p);
where 2 represents the location of print within the associated virtual function table.因為調用print是通過函數指針_vptr[2]進行的,是以,編譯器不能靜态的決定這個調用位址,并且,這個函數也不能内聯。
當然,虛函數print的内聯實體(definition)也必須在某個地方表現出來。 即是說,至少有一個函數實體是在virtual table調用的位址原地展開的。編譯器是如何決定在何時展開這個函數實體呢?其中一個編譯(implementaion)政策是當virtual table生成的同時,生成這個函數實體。這就是說對于每一個派生類的virtual table都會生成一個函數實體。
在一個可應用的類[b]中有多少vitrual table會被生成呢?呵呵,這是一個好問題。C++标準中對虛函數行為進行了規定,但是沒有對函數實作進行規定。由于virtual table沒有在C++标準中進行規定,很明顯,究竟這個virtual table怎樣生成,和究竟要生成多少個vitrual table也沒有規定。多少個?當然,我們隻要一個。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。新的規則要求編譯器隻展開一個内聯虛函數。如果一點被廣泛采用的話,虛函數的内聯導緻的代碼膨脹問題就會消失。