Trait是C++模板設計的一個基本應用技巧,通過應用Trait模板參數,可以在工業強度的程度設計中既保證了程序的靈活性,同時也有效的減少了類型參數的數量。對於普通函數而言,如果想要對該函數的功能進行更為細粒化的控制,一個主要的方法便是為該函數增加更多的參數,而函數體內的邏輯代碼則會根據參數值的不同而選擇不同的邏輯分支,最終返回不同的結果。一個極端的現象是函數的參數過多,而且並不是每個參數都會為每個調用者所用,如果簡單的使用缺省值,那么這些缺省參數則必須位於函數參數的末尾。如果有朝一日需要新增函數參數,而該參數恰恰又沒有缺省值,此時我們只能將該新增的參數放到所有缺省參數的前面,一旦如此,已有的函數調用者都將不得不進行相應的修改以適應該函數簽名的改變。同時隨着參數的增多,也會給調用者帶來額外的負擔。一個更好的方法是將函數參數改為結構體的字段,而函數的參數也調整為該結構體的指針或者引用。相比於此,Trait模板類可以被視為剛剛提到的結構體,但是不同的是該Trait類的行為需要依賴另外一個模板參數,既當前類的主模板參數。為了更好的理解Trait在模板中的應用場景,我們先給出一個簡單的示例,並再一步一步將其改造為對Trait的應用,我想這樣將更加便於大家的理解。
這里是一個非常簡單的例子,但是卻非常適合Trait的學習。該示例函數的主要功能是遍歷模板參數類型的數組,並逐個累加每一個元素同時返回最后的累加值。
1 #include <stdio.h> 2 3 template<typename T> 4 T accumulate(T const* begin, T const* end) { 5 T total = T(); 6 while (begin != end) { 7 total += *begin; 8 ++begin; 9 } 10 return total; 11 } 12 13 int main() { 14 int test[5] = {1,2,3,4,5}; 15 char chartest[] = "templates"; 16 int r = accumulate(test,test + 5); 17 int r2 = accumulate(chartest, chartest + sizeof(chartest)); 18 printf("r is %d\n",r); 19 printf("r2 is %d\n",r2); 20 return 0; 21 } 22 //r is 15 23 //r2 is -49
對於上例的輸出結果,r2的值可能並不是我們想要的,那么為什么會出現這樣的結果呢?很簡單,在第二次調用模板函數accumulate的時候,模板參數被推演成char,而char的最大值為127,因此total的結果一旦超過該值就會變為負數。為了解決該問題,我們將不得不引入更多的模板參數作為函數的返回值類型,而返回值是不會參與類型推演的,所以在使用時就必須顯示指定該模板參數的實際類型。當然,我們也可以通過為主模板參數T引入一個與其關聯的trait模板類來解決該問題。見如下代碼和關鍵性注釋。
1 #include <stdio.h> 2 3 template<typename T> 4 class AccumulationTraits; //該類為trait模板類的基礎模板類,由於不會真實的使用,因此不需要定義。 5 6 //下面是針對每一個數值型的原始類型都特化了一個Trait模板類,這樣編譯器在實例化accumulate函數時 7 //會根據其類型參數的不同選擇不同的特化Trait類。 8 template<> 9 class AccumulationTraits<char> { 10 public: 11 typedef int AccT; 12 }; 13 14 template<> 15 class AccumulationTraits<short> { 16 public: 17 typedef int AccT; 18 }; 19 20 template<> 21 class AccumulationTraits<int> { 22 public: 23 typedef long long AccT; 24 }; 25 26 template<> 27 class AccumulationTraits<unsigned int> { 28 public: 29 typedef unsigned long long AccT; 30 }; 31 32 template<> 33 class AccumulationTraits<float> { 34 public: 35 typedef double AccT; 36 }; 37 38 //這里將該函數的返回值類型改為主模板參數T的trait類的指定屬性。 39 template<typename T> 40 typename AccumulationTraits<T>::AccT accumulate(T const* begin, T const* end) { 41 typedef typename AccumulationTraits<T>::AccT AccT; 42 AccT total = AccT(); 43 while (begin != end) { 44 total += *begin; 45 ++begin; 46 } 47 return total; 48 } 49 50 int main() { 51 int test[5] = {1,2,3,4,5}; 52 char chartest[] = "templates"; 53 int r = accumulate(test,test + 5); 54 int r2 = accumulate(chartest, chartest + sizeof(chartest)); 55 printf("r is %d\n",r); 56 printf("r2 is %d\n",r2); 57 return 0; 58 } 59 //r is 15 60 //r2 is 975
盡管通過Trait解決了返回值越界的問題,但是上面的代碼示例仍然存在一些問題,如:AccT total = AccT(),這行代碼要求AccT類型必須是原始類型,或者是有缺省構造的類類型,否則將會導致編譯錯誤。在這里,我們可以進一步升級Trait類,以便accumulate函數能夠通過Trait的一個屬性獲得total的缺省初始值。如:
1 #include <stdio.h> 2 3 template<typename T> 4 class AccumulationTraits; 5 6 //相比於上面的示例,該示例為每一個trait的特化類都新增了一個屬性,即zero()函數,用來表示 7 //AccT類型的缺省初始值。這里之所以用zero函數,而不是普通的常量值,是因為C++中要求const 8 //限定符修飾的成員變量只能是整數類型或枚舉類型,而不能是浮點或其他自定義類型。 9 template<> 10 class AccumulationTraits<char> { 11 public: 12 typedef int AccT; 13 static AccT zero() { return 0; } 14 }; 15 16 template<> 17 class AccumulationTraits<short> { 18 public: 19 typedef int AccT; 20 static AccT zero() { return 0; } 21 }; 22 23 template<> 24 class AccumulationTraits<int> { 25 public: 26 typedef long long AccT; 27 static AccT zero() { return 0; } 28 }; 29 30 template<> 31 class AccumulationTraits<unsigned int> { 32 public: 33 typedef unsigned long long AccT; 34 static AccT zero() { return 0; } 35 }; 36 37 template<> 38 class AccumulationTraits<float> { 39 public: 40 typedef double AccT; 41 static AccT zero() { return 0; } 42 }; 43 44 template<typename T> 45 typename AccumulationTraits<T>::AccT accumulate(T const* begin, T const* end) { 46 typedef typename AccumulationTraits<T>::AccT AccT; 47 AccT total = AccumulationTraits<T>::zero(); 48 while (begin != end) { 49 total += *begin; 50 ++begin; 51 } 52 return total; 53 }
相比於之前的代碼,上述示例盡管已經有了很大的改進,但是仍然存在一個明確的缺陷。在有些情況下,我們希望accumulate函數能夠使用我們自定義的Trait類,而不是之前已經定義的一組AccumulationTrait模板類,為了解決這一問題,可以考慮為該函數增加一個模板參數,即Trait的類型,同時也為該模板參數提供缺省值,即AccumulationTrait。然而事與願違,C++的標准並不允許模板函數包含缺省模板參數,因此我們不得不進行必要的變通,即將accumulate函數改為模板類的靜態成員函數,如:
1 template<typename T, typename AT = AccumulationTraits<T> > 2 class Accum { 3 public: 4 static typename AT::AccT accumulate(T const* begin, T const* end) { 5 typename AT::AccT total = AT::zero(); 6 while (begin != end) { 7 total += *begin; 8 ++begin; 9 } 10 return total; 11 } 12 }; 13 //由於模板類在實例化的過程中無法根據參數進行類型推演,因此這里我們提供了一個模板 14 //函數便於調用者的使用,又因為在大多數情況下我們都會使用缺省trait,只有在個別的 15 //情況下才會考慮使用自定義的trait類,因此我們提供的第一個輔助函數將僅包含一個模板參數。 16 template<typename T> 17 typename AccumulationTrait<T>::AccT accumulate(T const* begin, T const* end) { 18 return Accum<T>::accumulate(begin,end); 19 } 20 //這里提供的第二個輔助函數,將允許調用者使用自定義的Trait類,這里之所以將T作為第二個 21 //參數,是因為該類型可以通過函數的參數推演出來,因此調用者在使用時可以不用顯示指定該 22 //模板參數的實際類型,而是直接通過函數參數進行推演即可。凡是使用該輔助函數的調用者, 23 //都是希望提供自定義的Trait類,否則就會直接調用上一個輔助函數。 24 template<typename Traits,typename T> 25 typename Traits::AccT accumulate(T const* begin, T const* end) { 26 return Accum<T, Traits>::accum(begin,end); 27 }