1. 程式人生 > >Item 44:將引數無關程式碼重構到模板外去

Item 44:將引數無關程式碼重構到模板外去

Item 44: Factor parameter-independent code out of templates.

模板是個好東西,你可以在實現型別安全的同時少寫很多程式碼。但模板提供的是編譯期的多型, 即使你的程式碼看起來非常簡潔短小,生成的二進位制檔案也可能包含大量的冗餘程式碼。 因為模板每次例項化都會生成一個完整的副本,所以其中與模板引數無關的部分會造成程式碼膨脹(code bloat)。

把模板中引數無關的程式碼重構到模板外便可以有效地控制模板產生的程式碼膨脹。 另外程式碼膨脹也可以由型別模板引數產生:

  • 對於非型別模板引數產生的程式碼膨脹,用函式引數或資料成員來代替模板引數即可消除冗餘;
  • 對於型別模板引數產生的程式碼膨脹,可以讓不同例項化的模板類共用同樣的二進位制表示。

抽取公共程式碼

在避免程式碼冗餘的問題上,抽取公共程式碼(commonality and variability analysis)是我們每天都在用的方法。 當你寫幾個函式時,會把其中的公共部分抽取到另一個函式;當你宣告類時,也會把它們的公共部分抽取到父類中。

於是你希望在模板程式設計中也用該辦法來避免程式碼重複,但模板和非模板程式碼在這一點上是不同的:

  • 非模板的程式碼中,冗餘的顯式的(explicit)。只要有重複程式碼你都會看到它;
  • 模板程式碼中,冗餘是隱式的(implicit)。模板程式碼只有一份,模板被例項化時產生的冗餘需要你的直覺才能感受到。

模板產生的程式碼膨脹

現在來看一個模板是怎樣引發程式碼膨脹的。比如要實現一個固定大小的矩陣,它支援轉置運算。

template<typename T, int n>
class Square{
public:
    void invert();
};
其中的int n是一個非型別引數,它也是一種合法的模板引數~

然後可能會這樣使用該模板:

Square<double, 5> s1;
Square<double, 10> s2;
s1.invert();    s2.invert();

Square模板會例項化兩個類:Square

抽取父類模板

結局模板產生的程式碼膨脹,仍然是用抽取公共程式碼的辦法。如果你真的看到了二進位制程式碼中兩個相同的invert函式, 你的直覺肯定是把它抽取到另一個類中:

template<typename T>
class SquareBase{
protected:
    void invert(int size);
};

template<typename T, int n>
class Square:private SquareBase<T>{
private:
    using SquareBase<T>::invert;
public:
    void invert(){ this->invert(n); }
}

因為invert函式定義在基類中,所以它只會在二進位制程式碼中出現一次,即SquareBase::invert。該函式由兩個子類共享。 上述程式碼中有些細節還值得一提:

  • SquareBase::invert是供子類用的,所以宣告為private而不是public;
  • 呼叫父類invert的代價為零,因為Square::invert是隱式的inline函式,見Item 30
  • 使用this->字首是因為,SquareBase裡的名稱在子類模板Square裡是隱藏的,見Item 43
  • 使用private繼承是因為,Square is implemented in terms of Square,見Item 39

資料儲存問題

既然我們決定由父類來做invert操作,那麼父類怎麼訪問資料呢?因為資料本來是在子類中的。 當然我們可以在呼叫SquareBase::invert時把記憶體地址也一起告知父類, 但如果矩陣類中有很多函式都需要這些資訊呢?我們可能需要呼叫每個函式時都把這些資訊傳遞給父類函式。 既然這樣,何不把資料地址直接放在父類中?既然父類存放了資料,那麼就把矩陣大小也一併存放了吧!

template<typename T>
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< int * >, List< const int * >, List< double * >的底層實現也是一樣的。但因為指標型別不同,也會例項化為多份模板類。