Item 44: Factor parameter-independent code out of templates.
模板是個好東西,你可以在實現類型安全的同時少寫很多代碼。但模板提供的是編譯期的多態, 即使你的代碼看起來非常簡潔短小,生成的二進制文件也可能包含大量的冗余代碼。 因為模板每次實例化都會生成一個完整的副本,所以其中與模板參數無關的部分會造成代碼膨脹(code bloat)。
把模板中參數無關的代碼重構到模板外便可以有效地控制模板產生的代碼膨脹。 另外代碼膨脹也可以由類型模板參數產生:
在避免代碼冗余的問題上,抽取公共代碼(commonality and variability analysis)是我們每天都在用的方法。 當你寫幾個函數時,會把其中的公共部分抽取到另一個函數;當你聲明類時,也會把它們的公共部分抽取到父類中。
於是你希望在模板編程中也用該辦法來避免代碼重復,但模板和非模板代碼在這一點上是不同的:
現在來看一個模板是怎樣引發代碼膨脹的。比如要實現一個固定大小的矩陣,它支持轉置運算。
template
class Square{
public:
void invert();
};
其中的
int n
是一個非類型參數,它也是一種合法的模板參數~
然後可能會這樣使用該模板:
Square s1;
Square s2;
s1.invert(); s2.invert();
Square
模板會實例化兩個類:Square
和Square
,它們擁有相同的invert
方法。 這是模板產生代碼膨脹的典型場景。
結局模板產生的代碼膨脹,仍然是用抽取公共代碼的辦法。如果你真的看到了二進制代碼中兩個相同的invert
函數, 你的直覺肯定是把它抽取到另一個類中:
template
class SquareBase{
protected:
void invert(int size);
};
template
class Square:private SquareBase{
private:
using SquareBase::invert;
public:
void invert(){ this->invert(n); }
}
因為invert
函數定義在基類中,所以它只會在二進制代碼中出現一次,即SquareBase
。該函數由兩個子類共享。 上述代碼中有些細節還值得一提:
SquareBase::invert
是供子類用的,所以聲明為private
而不是public
;invert
的代價為零,因為Square::invert
是隱式的inline函數,見Item 30;this->
前綴是因為,SquareBase
裡的名稱在子類模板Square
裡是隱藏的,見Item 43;private
繼承是因為,Square
is implemented in terms ofSquare
,見Item 39。
既然我們決定由父類來做invert
操作,那麼父類怎麼訪問數據呢?因為數據本來是在子類中的。 當然我們可以在調用SquareBase::invert
時把內存地址也一起告知父類, 但如果矩陣類中有很多函數都需要這些信息呢?我們可能需要調用每個函數時都把這些信息傳遞給父類函數。 既然這樣,何不把數據地址直接放在父類中?既然父類存放了數據,那麼就把矩陣大小也一並存放了吧!
template
class SquareBase{
protected:
SquareBase(int _n, T *_data): n(_n), data(_data){}
void setData(T *_data){
data = _data;
}
private:
int n;
T* data;
};
父類中存儲了矩陣數據的位置(data
)以及大小(n
),子類仍然可以決定如何分配地址空間。 可以存放在子類中作為成員屬性,也可以動態申請內存。
不管數據是怎樣分配和訪問的,我們消除代碼重復的方案是確定的:將公共部分抽取到父模板類中。 這樣做的好處便是避免了代碼膨脹,減小了二進制文件和”working set”的大小,有利於提高指令緩存的命中率, 從而達到更高的代碼執行效率。但提取公共部分到新的模板類也造成了一些問題:
int n
硬編碼在模板參數中的話,編譯器能做更多的優化,比如常量傳播等。但int n
作為函數參數,這些優化就沒有了。T* data
指針。
實踐中到底是否應該抽取公共代碼出來取決於你的應用場景,在上述的優劣中進行權衡。
本問討論的是非類型模板參數,對於類型模板參數,代碼膨脹的問題也是存在的,比如
int
和long
在多數平台都是一樣的底層實現,然而模板卻會實例化為兩份,因為它們類型不同。List
,List
,List
的底層實現也是一樣的。但因為指針類型不同,也會實例化為多份模板類。