從模板中分離出引數無關的程式碼(轉)

ba發表於2007-08-15
從模板中分離出引數無關的程式碼(轉)[@more@]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(複合)使得原來的 classes 可以訪問這些通用特性。原來的 classes 中不同的構件——變化的構件——仍保留在它們原來的位置。

在寫 templates(模板)時,你要做同樣的分析,而且用同樣的方法避免重複,但這裡有一個技巧。在 non-template code(非模板程式碼)中,重複是顯式的:你可以看到兩個函式或兩個類之間存在重複。在 template code(模板程式碼)中。重複是隱式的:僅有一份 template(模板)原始碼的複製,所以你必須培養自己去判斷在一個 template(模板)被例項化多次後可能發生的重複。

例如,假設你要為固定大小的 square matrices(正方矩陣)寫一個 templates(模板),其中,要支援 matrix inversion(矩陣轉置)。

template
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 sm1;
...
sm1.invert(); // call SquareMatrix::invert

SquareMatrix sm2;
...
sm2.invert(); // call SquareMatrix::invert

這裡將有兩個 invert 的複製被例項化。這兩個函式不是相同的,因為一個作用於 5 x 5 矩陣,而另一個作用於 10 x 10 矩陣,但是除了常數 5 和 10 以外,這兩個函式是相同的。這是一個發生 template-induced code bloat(模板導致的程式碼膨脹)的經典方法。

如果你看到兩個函式除了一個版本使用了 5 而另一個使用了 10 之外,對應字元全部相等,你該怎麼做呢?你的直覺讓你建立一個取得一個值作為一個引數的函式版本,然後用 5 或 10 呼叫這個引數化的函式以代替複製程式碼。你的直覺為你提供了很好的方法!以下是一個初步過關的 SquareMatrix 的做法:

template // 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 {
private:
 using SquareMatrixBase::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 是隱式的——參見《理解inline化的介入和排除》。)這些函式使用了 "this->" 標記,因為就像 Item 43 解釋的,如果不這樣,在 templatized base classes(模板化基類)中的函式名(諸如 SquareMatrixBase)被 derived classes(派生類)隱藏。還要注意 SquareMatrix 和 SquareMatrixBase 之間的繼承關係是 private 的。這準確地反映了 base class(基類)存在的理由僅僅是簡化 derived classes(派生類)的實現的事實,而不是表示 SquareMatrix 和 SquareMatrixBase 之間的一個概念上的 is-a 關係。(關於 private inheritance(私有繼承)的資訊,參見 《謹慎使用私有繼承》。)

迄今為止,還不錯,但是有一個棘手的問題我們還沒有提及。SquareMatrixBase::invert 怎樣知道應操作什麼資料?它從它的引數知道矩陣的大小,但是它怎樣知道一個特定矩陣的資料在哪裡呢?大概只有 derived class(派生類)才知道這些。derived class(派生類)如何把這些傳達給 base class(基類)以便於 base class(基類)能夠做這個轉置呢?

一種可能是為 SquareMatrixBase::invert 增加另一個的引數,也許是一個指向儲存矩陣資料的記憶體塊的開始位置的指標。這樣可以工作,但是十有八九,invert 不是 SquareMatrix 中僅有的能被寫成一種 size-independent(大小無關)的方式並移入 SquareMatrixBase 的函式。如果有幾個這樣的函式,全都需要一種找到持有矩陣內的值的記憶體的方法。我們可以為它們全都增加一個額外的引數,但是我們一再重複地告訴 SquareMatrixBase 同樣的資訊。這看上去不太正常。

一個可替換方案是讓 SquareMatrixBase 儲存一個指向矩陣的值的記憶體區域的指標。而且一旦它儲存了這個指標,它同樣也可以儲存矩陣大小。最後得到的設計大致就像這樣:

template
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
class SquareMatrix: private SquareMatrixBase {
public:
SquareMatrix() // send matrix size and
: SquareMatrixBase(n, data) {} // data ptr to base class
...

private:
T data[n*n];
};

這種型別的 objects 不需要 dynamic memory allocation(動態記憶體分配),但是這些 objects 本身可能會非常大。一個可選方案是將每一個矩陣的資料放到 heap(堆)上:

template
class SquareMatrix: private SquareMatrixBase {
public:
SquareMatrix() // set base class data ptr to null,
: SquareMatrixBase(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 pData; // see Item 13 for info on
}; // boost::scoped_array

無論資料儲存在哪裡,從膨脹的觀點來看關鍵的結果在於:現在 SquareMatrix 的許多——也許是全部—— member functions(成員函式)可以簡單地 inline 呼叫它的 base class versions(基類版本),而這個版本是與其它所有持有相同資料型別的矩陣共享的,而無論它們的大小。與此同時,不同大小的 SquareMatrix objects 是截然不同的型別,所以,例如,即使 SquareMatrix 和 SquareMatrix objects 使用 SquareMatrixBase 中同樣的 member functions(成員函式),也沒有機會將一個 SquareMatrix object 傳送給一個期望一個 SquareMatrix 的函式。很好,不是嗎?

很好,是的,但不是免費的。將矩陣大小硬性固定在其中的 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 指標導致封裝性的降低。它也可能導致資源管理複雜化:如果 base class(基類)儲存了一個指向矩陣資料的指標,但是那些資料既可以是動態分配的也可以是物理地儲存於 derived class object(派生類物件)之內的(就像我們看到的),它如何決定這個指標是否應該被刪除?這樣的問題有答案,但是你越想讓它們更加精巧一些,它就會變成更復雜的事情。在某些條件下,少量的程式碼重複就像是一種解脫。

本文只討論了由於 non-type template parameters(非型別模板引數)引起的膨脹,但是 type parameters(型別引數)也能導致膨脹。例如,在很多平臺上,int 和 long 有相同的二進位制表示,所以,可以說,vector 和 vector 的 member functions(成員函式)很可能是相同的——膨脹的恰到好處的解釋。某些連線程式會合並同樣的函式實現,還有一些不會,而這就意味著在一些環境上一些模板在 int 和 long 上都被例項化而能夠引起程式碼重複。類似地,在大多數平臺上,所有的指標型別有相同的二進位制表示,所以持有指標型別的模板(例如,list,list,list*> 等)應該通常可以使用每一個 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(型別引數)引起的膨脹可以透過讓具有相同的二進位制表示的例項化型別共享實現而減少。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617731/viewspace-957403/,如需轉載,請註明出處,否則將追究法律責任。

相關文章