# Item 44: 從 templates(模板)中分離出 parameter-independent(參數無關)的代碼
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發布:http://blog.csdn.net/fatalerror99/
templates(模板)是節省時間和避免代碼重復的極好方法。不必再輸入 20 個相似的 classes,每一個包含 15 個 member functions(成員函數),你可以輸入一個 class template(類模板),并讓編譯器實例化出你需要的 20 個 specific classes(特定類)和 300 個函數。(class template(類模板)的 member functions(成員函數)只有被使用時才會被隱式實例化,所以只有在每一個函數都被實際使用時,你才會得到全部 300 個member functions(成員函數)。)function templates(函數模板)也有相似的魅力。不必再寫很多函數,你可以寫一個 function templates(函數模板)并讓編譯器做其余的事。這不是很重要的技術嗎?
是的,不錯……有時。如果你不小心,使用 templates(模板)可能導致 code bloat(代碼膨脹):重復的(或幾乎重復的)的代碼,數據,或兩者都有的二進制碼。結果會使源代碼看上去緊湊而整潔,但是目標代碼臃腫而松散。臃腫而松散很少會成為時尚,所以你需要了解如何避免這樣的二進制擴張。
你的主要工具有一個有氣勢的名字 commonality and variability analysis(通用性與可變性分析),但是關于這個想法并沒有什么有氣勢的東西。即使在你的職業生涯中從來沒有使用過模板,你也應該從始至終做這樣的分析。
當你寫一個函數,而且你意識到這個函數的實現的某些部分和另一個函數的實現本質上是相同的,你會僅僅復制代碼嗎?當然不。你從這兩個函數中分離出通用的代碼,放到第三個函數中,并讓那兩個函數來調用這個新的函數。也就是說,你分析那兩個函數以找出那些通用和變化的構件,你把通用的構件移入一個新的函數,并把變化的構件保留在原函數中。類似地,如果你寫一個 class,而且你意識到這個 class 的某些構件和另一個 class 的構件是相同的,你不要復制那些通用構件。作為替代,你把通用構件移入一個新的 class 中,然后你使用 inheritance(繼承)或 composition(復合)(參見 Items 32,38 和 39)使得原來的 classes 可以訪問這些通用特性。原來的 classes 中不同的構件——變化的構件——仍保留在它們原來的位置。
在寫 templates(模板)時,你要做同樣的分析,而且用同樣的方法避免重復,但這里有一個技巧。在 non-template code(非模板代碼)中,重復是顯式的:你可以看到兩個函數或兩個類之間存在重復。在 template code(模板代碼)中。重復是隱式的:僅有一份 template(模板)源代碼的拷貝,所以你必須培養自己去判斷在一個 template(模板)被實例化多次后可能發生的重復。
例如,假設你要為固定大小的 square matrices(正方矩陣)寫一個 templates(模板),其中,要支持 matrix inversion(矩陣轉置)。
```
template<typename T, // template for n x n matrices of
std::size_t n> // objects of type T; see below for info
class SquareMatrix { // on the size_t parameter
public:
...
void invert(); // invert the matrix in place
};
```
這個 template(模板)取得一個 type parameter(類型參數)T,但是它還有一個類型為 size_t 的參數——一個 non-type parameter(非類型參數)。non-type parameter(非類型參數)比 type parameter(類型參數)更不通用,但是它們是完全合法的,而且,就像在本例中,它們可以非常自然。
現在考慮以下代碼:
```
SquareMatrix<double, 5> sm1;
...
sm1.invert(); // call SquareMatrix<double, 5>::invert
SquareMatrix<double, 10> sm2;
...
sm2.invert(); // call SquareMatrix<double, 10>::invert
```
這里將有兩個 invert 的拷貝被實例化。這兩個函數不是相同的,因為一個作用于 5 x 5 矩陣,而另一個作用于 10 x 10 矩陣,但是除了常數 5 和 10 以外,這兩個函數是相同的。這是一個發生 template-induced code bloat(模板導致的代碼膨脹)的經典方法。
如果你看到兩個函數除了一個版本使用了 5 而另一個使用了 10 之外,對應字符全部相等,你該怎么做呢?你的直覺讓你創建一個取得一個值作為一個參數的函數版本,然后用 5 或 10 調用這個參數化的函數以代替復制代碼。你的直覺為你提供了很好的方法!以下是一個初步過關的 SquareMatrix 的做法:
```
template<typename T> // size-independent base class for
class SquareMatrixBase { // square matrices
protected:
...
void invert(std::size_t matrixSize); // invert matrix of the given size
...
};
template< typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
private:
using SquareMatrixBase<T>::invert; // avoid hiding base version of
// invert; see Item 33
public:
...
void invert() { this->invert(n); } // make inline call to base class
}; // version of invert; see below
// for why "this->" is here
```
就像你能看到的,invert 的參數化版本是在一個 base class(基類)SquareMatrixBase 中的。與 SquareMatrix 一樣,SquareMatrixBase 是一個 template(模板),但與 SquareMatrix 不一樣的是,它參數化的僅僅是矩陣中的對象的類型,而沒有矩陣的大小。因此,所有持有一個給定對象類型的矩陣將共享一個單一的 SquareMatrixBase class。從而,它們共享 invert 在那個 class 中的版本的單一拷貝。
SquareMatrixBase::invert 僅僅是一個計劃用于 derived classes(派生類)以避免代碼重復的方法,所以它是 protected 的而不是 public 的。調用它的額外成本應該為零,因為 derived classes(派生類)的 inverts 使用 inline functions(內聯函數)調用 base class(基類)的版本。(這個 inline 是隱式的——參見 Item 30。)這些函數使用了 "this->" 標記,因為就像 Item 43 解釋的,如果不這樣,在 templatized base classes(模板化基類)中的函數名(諸如 SquareMatrixBase<T>)被 derived classes(派生類)隱藏。還要注意 SquareMatrix 和 SquareMatrixBase 之間的繼承關系是 private 的。這準確地反映了 base class(基類)存在的理由僅僅是簡化 derived classes(派生類)的實現的事實,而不是表示 SquareMatrix 和 SquareMatrixBase 之間的一個概念上的 is-a 關系。(關于 private inheritance(私有繼承)的信息,參見 Item 39。)
迄今為止,還不錯,但是有一個棘手的問題我們還沒有提及。SquareMatrixBase::invert 怎樣知道應操作什么數據?它從它的參數知道矩陣的大小,但是它怎樣知道一個特定矩陣的數據在哪里呢?大概只有 derived class(派生類)才知道這些。derived class(派生類)如何把這些傳達給 base class(基類)以便于 base class(基類)能夠做這個轉置呢?
一種可能是為 SquareMatrixBase::invert 增加另一個的參數,也許是一個指向存儲矩陣數據的內存塊的開始位置的指針。這樣可以工作,但是十有八九,invert 不是 SquareMatrix 中僅有的能被寫成一種 size-independent(大小無關)的方式并移入 SquareMatrixBase 的函數。如果有幾個這樣的函數,全都需要一種找到持有矩陣內的值的內存的方法。我們可以為它們全都增加一個額外的參數,但是我們一再重復地告訴 SquareMatrixBase 同樣的信息。這看上去不太正常。
一個可替換方案是讓 SquareMatrixBase 存儲一個指向矩陣的值的內存區域的指針。而且一旦它存儲了這個指針,它同樣也可以存儲矩陣大小。最后得到的設計大致就像這樣:
```
template<typename T>
class SquareMatrixBase {
protected:
SquareMatrixBase(std::size_t n, T *pMem) // store matrix size and a
: size(n), pData(pMem) {} // ptr to matrix values
void setDataPtr(T *ptr) { pData = ptr; } // reassign pData
...
private:
std::size_t size; // size of matrix
T *pData; // pointer to matrix values
};
```
這樣就是讓 derived classes(派生類)決定如何分配內存。某些實現可能決定直接在 SquareMatrix object 內部存儲矩陣數據:
```
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
public:
SquareMatrix() // send matrix size and
: SquareMatrixBase<T>(n, data) {} // data ptr to base class
...
private:
T data[n*n];
};
```
這種類型的 objects 不需要 dynamic memory allocation(動態內存分配),但是這些 objects 本身可能會非常大。一個可選方案是將每一個矩陣的數據放到 heap(堆)上:
```
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
public:
SquareMatrix() // set base class data ptr to null,
: SquareMatrixBase<T>(n, 0), // allocate memory for matrix
pData(new T[n*n]) // values, save a ptr to the
{ this->setDataPtr(pData.get()); } // memory, and give a copy of it
... // to the base class
private:
boost::scoped_array<T> pData; // see Item 13 for info on
}; // boost::scoped_array
```
無論數據存儲在哪里,從膨脹的觀點來看關鍵的結果在于:現在 SquareMatrix 的許多——也許是全部—— member functions(成員函數)可以簡單地 inline 調用它的 base class versions(基類版本),而這個版本是與其它所有持有相同數據類型的矩陣共享的,而無論它們的大小。與此同時,不同大小的 SquareMatrix objects 是截然不同的類型,所以,例如,即使 SquareMatrix<double, 5> 和 SquareMatrix<double, 10> objects 使用 SquareMatrixBase<double> 中同樣的 member functions(成員函數),也沒有機會將一個 SquareMatrix<double, 5> object 傳送給一個期望一個 SquareMatrix<double, 10> 的函數。很好,不是嗎?
很好,是的,但不是免費的。將矩陣大小硬性固定在其中的 invert 版本很可能比將大小作為一個函數參數傳入或存儲在 object 中的共享版本能產生更好的代碼。例如,在 size-specific(特定大小)的版本中,sizes(大小)將成為 compile-time constants(編譯期常數),因此適用于像 constant propagation 這樣的優化,包括將它們作為 immediate operands(立即操作數)嵌入到生成的指令中。在 size-independent version(大小無關版本)中這是不可能做到的。
另一方面,將唯一的 invert 的版本用于多種矩陣大小縮小了可執行碼的大小,而且還能縮小程序的 working set(工作區)大小以及改善 instruction cache(指令緩存)中的 locality of reference(引用的局部性)。這些能使程序運行得更快,超額償還了失去的針對 invert 的 size-specific versions(特定大小版本)的任何優化。哪一個效果更劃算?唯一的分辨方法就是在你的特定平臺和典型數據集上試驗兩種方法并觀察其行為。
另一個效率考慮關系到 objects 的大小。如果你不小心,將函數的 size-independent 版本(大小無關版本)上移到一個 base class(基類)中會增加每一個 object 的整體大小。例如,在我剛才展示的代碼中,即使每一個 derived class(派生類)都已經有了一個取得數據的方法,每一個 SquareMatrix object 都還有一個指向它的數據的指針存在于 SquareMatrixBase class 中,這為每一個 SquareMatrix object 至少增加了一個指針的大小。通過改變設計使這些指針不再必需是有可能的,但是,這又是一樁交易。例如,讓 base class(基類)存儲一個指向矩陣數據的 protected 指針導致在 Item 22 中描述的封裝性的降低。它也可能導致資源管理復雜化:如果 base class(基類)存儲了一個指向矩陣數據的指針,但是那些數據既可以是動態分配的也可以是物理地存儲于 derived class object(派生類對象)之內的(就像我們看到的),它如何決定這個指針是否應該被刪除?這樣的問題有答案,但是你越想讓它們更加精巧一些,它就會變成更復雜的事情。在某些條件下,少量的代碼重復就像是一種解脫。
本 Item 只討論了由于 non-type template parameters(非類型模板參數)引起的膨脹,但是 type parameters(類型參數)也能導致膨脹。例如,在很多平臺上,int 和 long 有相同的二進制表示,所以,可以說,vector<int> 和 vector<long> 的 member functions(成員函數)很可能是相同的——膨脹的恰到好處的解釋。某些連接程序會合并同樣的函數實現,還有一些不會,而這就意味著在一些環境上一些模板在 int 和 long 上都被實例化而能夠引起代碼重復。類似地,在大多數平臺上,所有的指針類型有相同的二進制表示,所以持有指針類型的模板(例如,list<int\*>,list<const int\*>,list<SquareMatrix<long, 3>\*> 等)應該通常可以使用每一個 member function(成員函數)的單一的底層實現。典型情況下,這意味著與 strongly typed pointers(強類型指針)(也就是 T\* 指針)一起工作的 member functions(成員函數)可以通過讓它們調用與 untyped pointers(無類型指針)(也就是 void\* 指針)一起工作的函數來實現。一些標準 C++ 庫的實現對于像 vector,deque 和 list 這樣的模板就是這樣做的。如果你關心起因于你的模板的代碼膨脹,你可能需要用同樣的做法開發模板。
Things to Remember
* templates(模板)產生多個 classes 和多個 functions,所以一些不依賴于 template parameter(模板參數)的模板代碼會引起膨脹。
* non-type template parameters(非類型模板參數)引起的膨脹常常可以通過用 function parameters(函數參數)或 class data members(類數據成員)替換 template parameters(模板參數)而消除。
* type parameters(類型參數)引起的膨脹可以通過讓具有相同的二進制表示的實例化類型共享實現而減少。
- Preface(前言)
- Introduction(導言)
- Terminology(術語)
- Item 1: 將 C++ 視為 federation of languages(語言聯合體)
- Item 2: 用 consts, enums 和 inlines 取代 #defines
- Item 3: 只要可能就用 const
- Item 4: 確保 objects(對象)在使用前被初始化
- Item 5: 了解 C++ 為你偷偷地加上和調用了什么函數
- Item 6: 如果你不想使用 compiler-generated functions(編譯器生成函數),就明確拒絕
- Item 7: 在 polymorphic base classes(多態基類)中將 destructors(析構函數)聲明為 virtual(虛擬)
- Item 8: 防止因為 exceptions(異常)而離開 destructors(析構函數)
- Item 9: 絕不要在 construction(構造)或 destruction(析構)期間調用 virtual functions(虛擬函數)
- Item 10: 讓 assignment operators(賦值運算符)返回一個 reference to *this(引向 *this 的引用)
- Item 11: 在 operator= 中處理 assignment to self(自賦值)
- Item 12: 拷貝一個對象的所有組成部分
- Item 13: 使用對象管理資源
- Item 14: 謹慎考慮資源管理類的拷貝行為
- Item 15: 在資源管理類中準備訪問裸資源(raw resources)
- Item 16: 使用相同形式的 new 和 delete
- Item 17: 在一個獨立的語句中將 new 出來的對象存入智能指針
- Item 18: 使接口易于正確使用,而難以錯誤使用
- Item 19: 視類設計為類型設計
- Item 20: 用 pass-by-reference-to-const(傳引用給 const)取代 pass-by-value(傳值)
- Item 21: 當你必須返回一個對象時不要試圖返回一個引用
- Item 22: 將數據成員聲明為 private
- Item 23: 用非成員非友元函數取代成員函數
- Item 24: 當類型轉換應該用于所有參數時,聲明為非成員函數
- Item 25: 考慮支持不拋異常的 swap
- Item 26: 只要有可能就推遲變量定義
- Item 27: 將強制轉型減到最少
- Item 28: 避免返回對象內部構件的“句柄”
- Item 29: 爭取異常安全(exception-safe)的代碼
- Item 30: 理解 inline 化的介入和排除
- Item 31: 最小化文件之間的編譯依賴
- Item 32: 確保 public inheritance 模擬 "is-a"
- Item 33: 避免覆蓋(hiding)“通過繼承得到的名字”
- Item 34: 區分 inheritance of interface(接口繼承)和 inheritance of implementation(實現繼承)
- Item 35: 考慮可選的 virtual functions(虛擬函數)的替代方法
- Item 36: 絕不要重定義一個 inherited non-virtual function(通過繼承得到的非虛擬函數)
- Item 37: 絕不要重定義一個函數的 inherited default parameter value(通過繼承得到的缺省參數值)
- Item 38: 通過 composition(復合)模擬 "has-a"(有一個)或 "is-implemented-in-terms-of"(是根據……實現的)
- Item 39: 謹慎使用 private inheritance(私有繼承)
- Item 40: 謹慎使用 multiple inheritance(多繼承)
- Item 41: 理解 implicit interfaces(隱式接口)和 compile-time polymorphism(編譯期多態)
- Item 42: 理解 typename 的兩個含義
- Item 43: 了解如何訪問 templatized base classes(模板化基類)中的名字
- Item 44: 從 templates(模板)中分離出 parameter-independent(參數無關)的代碼
- Item 45: 用 member function templates(成員函數模板) 接受 "all compatible types"(“所有兼容類型”)
- Item 46: 需要 type conversions(類型轉換)時在 templates(模板)內定義 non-member functions(非成員函數)
- Item 47: 為類型信息使用 traits classes(特征類)
- Item 48: 感受 template metaprogramming(模板元編程)
- Item 49: 了解 new-handler 的行為
- Item 50: 領會何時替換 new 和 delete 才有意義
- Item 51: 編寫 new 和 delete 時要遵守慣例
- Item 52: 如果編寫了 placement new,就要編寫 placement delete
- 附錄 A. 超越 Effective C++
- 附錄 B. 第二和第三版之間的 Item 映射