第15章 trait與policy類
------------------------------------------------------------------------------------------------------------
模板讓我們可以針對多種類型對類和函數進行參數,但我們並不希望為了能夠最大程度地參數化而引入太多的模板參數,同時在客戶端指定所有的相應實參往往也是煩人的。我們知道我們希望引入的大多數額外參數都具有合理的缺省值。在某些情況下額外參數還可以有幾個主參數來確定。
policy類和trait(或者稱為trait模板)是兩種C++程序設計機制。它們有助於對某些額外參數的管理,這里的額外參數是指:在具有工業強度的模板設計中所出現的參數。
trait類:提供所需要的關於模板參數的類型的所有必要信息;(STL源碼大量運用了這種技巧)
policy類:有點像策略模式,通過policy類掛接不同的算法;
------------------------------------------------------------------------------------------------------------
15.1 一個實例:累加一個序列
15.1.1 fixed traits
// traits/accum1.hpp #ifndef ACCUM_HPP #define ACCUM_HPP template <typename T> inline T accum(T const* beg, T const* end) { T total = T(); // 假設T()事實上會產生一個等於0的值 while(beg != end) { total += *beg; ++beg; } return total; } #endif //ACCUM_HPP
考慮下面的調用過程:
// traits/accum1.cpp #include "accum1.hpp" #include <iostream> int main() { // 生成一個含有5個整數值的數組 int num[] = {1,2,3,4,5}; // 輸出平均值 std::cout << "the average value of the integer values is" << accum(&num[0], &num[5]) / 5 << '\n'; // 創建字符值數組 char name[] = "templates"; int length = sizeof(name) - 1; // (試圖)輸出平均的字符值 std::cout << "the average value of the characters in \"" << name << "\" is " << accum(&num[0], &num[length]) / length << '\n'; } 輸出: the average value of the integer values is 3 the average value of the characters in "templates" is -5
這里的問題是我們的模板是基於char類型進行實例化的,而char的范圍是很小的,即使對於相對較小的數值進行求和也可能會出現越界的情況。顯然,我們可以通過引入一個額外的模板參數AccT來解決這個問題,其中AccT描述了變量total的類型(同時也是返回類型)。然而,這將會給該模板的所有用戶都強加一個額外的負擔:他們每次調用這個模板的時候,都要指定這個額外的類型。因此,針對我們上面的例子,我們不得不這樣編寫代碼:
accum<int>(&name[0], &name[length])
雖然說這個約束並不會很麻煩,但我們仍然期望可以完全避免這個約束。
關於這個額外參數,另一種解決方案是對accum()所調用的每個T類型都創建一個關聯,所關聯的類型就是用來存儲累加和的類型。這種關聯可以被看作是類型T的一個特征,因此,我們也把這個存儲累加和的類型稱為T的trait。於是,我們可以導出我們的第一個trait類:
// traits/accumtraits2.hpp template<typename T> class AccumulationTraits; template<> class AccumulationTraits<char> { public: typedef int AccT; }; template<> class AccumulationTraits<char> { public: typedef int AccT; }; template<> class AccumulationTraits<short> { public: typedef int AccT; }; template<> class AccumulationTraits<int> { public: typedef long AccT; }; template<> class AccumulationTraits<unsigned int> { public: typedef unsigned long AccT; }; template<> class AccumulationTraits<float> { public: typedef double AccT; };
在上面代碼中,模板AccumulationTraits被稱為一個trait模板,因為它含有它的參數類型的一個trait(通常而言,可以存在多個trait和多個參數)。對這個模板,我們並不提供一個泛型的定義,因為在我們不知道參數類型的前提下,並不能確定應該選擇什么樣的類型作為和的類型。然而,我們可以利用某個實參類型,而T本身通常都能夠作為這樣的一個候選類型。這樣,我們可以改寫前面的accum()模板如下:
// traits/accum2.hpp #ifndef ACCUM_HPP #define ACCUM_HPP template<typename T> inline typename AccumulationTraits<T>::AccT accum(T const* beg, T const* end) { // 返回值的類型是一個元素類型的trait typedef typename AccumulationTraits<T>::AccT Acct; AccT total = AccT(); // 假設AccT()實際上生成了一個0值 while(beg != end) { total += *beg; ++beg; } return total; } #endif // ACCUM_HPP // 於是,現在例子程序的輸入完全符合我們的期望,如下: the average value of the integer values is 3 the average value of the characters in "templates" is 108
15.1.2 value trait
到目前為止,我們已經看到了trait可以用來表示:“主”類型所關聯的一些額外的類型信息。在這一小節里,我們將闡明這個額外的信息並不局限於類型,常數和其他類型的值也可以和一個類型進行關聯。
我們前面的accum()模板使用了缺省構造函數的返回值來初始化結果變量(即total),而且我們期望該返回值是一個類似0的值:
AccT total = AccT(); // 假設AccT()實際上生成了一個0值 ... return total;
顯然,我們並不能保證上面的構造函數會返回一個符合條件的值,可以用來開始這個求和循環。而且,類型AccT也不一定具有一個缺省構造函數。
在此,我們可以再次使用trait來解決這個問題。對於上面的例子,我們需要給AccumulationTraits添加一個value trait,最終選擇的方案如下(書籍介紹了最后選擇這種方案的原因,在此省略,詳見書籍):
// traits/accumtraits4.hpp template<typename T> class AccumulationTraits; template<> class AccumulationTraits<char> { public: typedef int AccT; // 之所以選擇使用靜態函數返回一個值,原因如下: // 方案1:直接定義“static AccT const zero = 0;”,缺點:在所在類的內部,C++只允許我們對整型和枚舉類型初始化成靜態成員變量 // 方案2:類內聲明“static double const zero;”,源文件進行初始化“double const AccumulationTraits<float>::zero = 0.0;”,
缺點:這種解決方法對編譯器而言是不可知的。也就是說,在處理客戶端文件的時候,編譯器通常都不會知道位於其他文件的定義 // 綜上,選擇了下面使用靜態函數返回所需要的值的方法 static AccT zero(){ return 0; } }; // 其他內建類型的特化版本類似 ......
對於應用程序代碼而言,唯一的區別只是這里使用了函數調用語法(而不是訪問一個靜態數據成員):
AccT total = AccumulationTraits<T>::zero();
顯然,trait還可以代表更多的類型。在我們的例子中,trait可以是一個機制,用於提供accum()所需要的、關於元素類型的所有必要信息;實際上,這個元素類型就是調用accum()的類型,即模板參數的類型。下面是trait概念的關鍵部分:trait提供了一種配置具體元素(通常是類型)的途徑,而該途徑主要是用於泛型計算。
在上一節所使用的trait被稱為fixed trait,因為一旦定義了這個分離的trait,就不能再算法中對它進行改寫。然而,在有些情況下我們需要對trait進行改寫。從原則上講,參數化trait主要的目的在於:添加一個具有缺省值的模板參數,而且該缺省值是由我們前面介紹的trait模板決定的。在這種具有缺省值的情況下,許多用戶就可以不需要提供這個額外的模板實參;但對於有特殊需求的用戶,也可以改寫這個預設的類型。
對於這個特殊的解決方案,唯一的不足在於:我們並不能對函數模板預設缺省模板實參。可以通過把算法實現為一個類,繞過這個不足。這同時也說明了:除了函數模板之外,在類模板中也可以很容易地使用trait,唯一的確點就是:類模板不能對它的模板參數進行演繹,而是必須顯式提供這些模板參數。因此,我們需要編寫如下形式的代碼: Accum<char>::accum(&name[0], &name[length]) 前面例子的代碼修改如下:
#ifndef ACCUM_HPP #define ACCUM_HPP #include "accumtraits4.hpp" template <typename T, typename AT = AccumulationTraits<T> > class Accum { public: static typename AT::AccT accum(T const* beg, T const* end) { typename AT::AccT total = AT::zero(); while (beg != end) { total += *beg; ++beg; } return total; } }; #endif // ACCUM_HPP
通常而言,大多數使用這個模板的用戶都不必顯式地提供第2個模板實參,因為我們可以針對第1個實參的類型,為每種類型都配置一個合適的缺省值。
和大多數情況一樣,我們可以引入一個輔助函數,來簡化上面基於類的接口:
template<typename T> inline typename AccumulationTraits<T>::AccT accum(T const* beg, T const* end) { // 第2個實參由類模板的缺省實參提供 return Accum<T>::accum(beg, end); } template<typename Traits, typename T> inline typename Traits<T>::AccT accum(T const* beg, T const* end) { // 第2個實參由Traits實參提供,替換缺省實參 return Accum<T, Traits>::accum(beg, end); }
15.1.4 policy 和 policy類(個人理解:有點像策略模式,掛接核心操作,改變算法行為)
到目前為止,我們把累積(accumulation)與求和(summation)等價起來了。事實上,還可以有其他種類的累積。例如,我們可以對序列中的給定值進行求積;如果這些值是字符串的話,還可以對它們進行連接。甚至於在一個序列中找到一個最大值,也可以被看成是累積問題的一種形式。在這所有的情況中,針對accum()的所有操作,唯一需要改變的只是“total += *beg;” 操作。於是,我們就把這個操作稱為該累積過程的一個policy。因此,一個policy類就是一個提供了一個接口的類,該接口能夠在算法中應用一個或多個policy。(個人理解:policy,核心操作的一個代理,通過替換policy,達到改變算法核心操作,從而改變算法行為的目的)
下面是一個例子,它說明了如何在我們的Accum類模板中引入這樣的一個接口:
// traits/accum6.hpp #ifndef ACCUM_HPP #define ACCUM_HPP #include "accumtraits4.hpp" #include "sumpolicy1.hpp" template <typename T, typename Policy = SumPolicy, typename Traits = AccumulationTraits<T> > class Accum { public: typedef typename Traits::AccT AccT; static AccT accum(T const* beg, T const* end) { AccT total = Traits::zero(); while (beg != end) { Policy::accumulate(total, *beg); ++beg; } return total; } }; #endif //ACCUM_HPP
其中SumPolicy類可以編寫如下:
// traits/sumpolicy1.hpp #ifndef SUMPOLICY_HPP #define SUMPOLICY_HPP class SumPolicy { public: template<typename T1, typename T2> // 成員模板 static void accumulate(T1& total, T2 const & value) { total += value; } }; #endif //SUMPOLICY_HPP
在這個例子中,我們把policy實現為一個具有一個成員函數模板的普通類(也就是說,類本身不是模板,而且該成員函數是隱式內聯的)。后面我們還會討論另一種實現方案。
通過給累積值指定一個不同的policy,我們就可以進行不同的計算。如下:
// traits/accum7.cpp #include "accum6.hpp" #include <iostream> class MultiPolicy { public: template<typename T1, typename T2> static void accumulate(T1& total, T2 const & value){ total *= value; } }; int main() { // 創建含有具有5個整型值的數組 int num[] = {1, 2, 3, 4, 5}; // 輸出所有值的乘積 std::cout << "the product of the integer values is " << Accum<int, MultiPolicy>::accum(&num[0], &num[5]) << '\n'; }
15.1.3 參數化trait
在上一節所使用的trait被稱為fixed trait,因為一旦定義了這個分離的trait,就不能再算法中對它進行改寫。然而,在有些情況下我們需要對trait進行改寫。從原則上講,參數化trait主要的目的在於:添加一個具有缺省值的模板參數,而且該缺省值是由我們前面介紹的trait模板決定的。在這種具有缺省值的情況下,許多用戶就可以不需要提供這個額外的模板實參;但對於有特殊需求的用戶,也可以改寫這個預設的類型。
對於這個特殊的解決方案,唯一的不足在於:我們並不能對函數模板預設缺省模板實參。可以通過把算法實現為一個類,繞過這個不足。這同時也說明了:除了函數模板之外,在類模板中也可以很容易地使用trait,唯一的確點就是:類模板不能對它的模板參數進行演繹,而是必須顯式提供這些模板參數。因此,我們需要編寫如下形式的代碼:
Accum<char>::accum(&name[0], &name[length])
前面例子的代碼修改如下:
#ifndef ACCUM_HPP #define ACCUM_HPP #include "accumtraits4.hpp" template <typename T, typename AT = AccumulationTraits<T> > class Accum { public: static typename AT::AccT accum(T const* beg, T const* end) { typename AT::AccT total = AT::zero(); while (beg != end) { total += *beg; ++beg; } return total; } }; #endif // ACCUM_HPP
通常而言,大多數使用這個模板的用戶都不必顯式地提供第2個模板實參,因為我們可以針對第1個實參的類型,為每種類型都配置一個合適的缺省值。
和大多數情況一樣,我們可以引入一個輔助函數,來簡化上面基於類的接口:
template<typename T> inline typename AccumulationTraits<T>::AccT accum(T const* beg, T const* end) { // 第2個實參由類模板的缺省實參提供 return Accum<T>::accum(beg, end); } template<typename Traits, typename T> inline typename Traits<T>::AccT accum(T const* beg, T const* end) { // 第2個實參由Traits實參提供,替換缺省實參 return Accum<T, Traits>::accum(beg, end); }
15.1.4 policy 和 policy類(個人理解:有點像策略模式,掛接核心操作,改變算法行為)
到目前為止,我們把累積(accumulation)與求和(summation)等價起來了。事實上,還可以有其他種類的累積。例如,我們可以對序列中的給定值進行求積;如果這些值是字符串的話,還可以對它們進行連接。甚至於在一個序列中找到一個最大值,也可以被看成是累積問題的一種形式。在這所有的情況中,針對accum()的所有操作,唯一需要改變的只是“total += *beg;” 操作。於是,我們就把這個操作稱為該累積過程的一個policy。因此,一個policy類就是一個提供了一個接口的類,該接口能夠在算法中應用一個或多個policy。(個人理解:policy,核心操作的一個代理,通過替換policy,達到改變算法核心操作,從而改變算法行為的目的)
下面是一個例子,它說明了如何在我們的Accum類模板中引入這樣的一個接口:
// traits/accum6.hpp #ifndef ACCUM_HPP #define ACCUM_HPP #include "accumtraits4.hpp" #include "sumpolicy1.hpp" template <typename T, typename Policy = SumPolicy, typename Traits = AccumulationTraits<T> > class Accum { public: typedef typename Traits::AccT AccT; static AccT accum(T const* beg, T const* end) { AccT total = Traits::zero(); while (beg != end) { Policy::accumulate(total, *beg); ++beg; } return total; } }; #endif //ACCUM_HPP
其中SumPolicy類可以編寫如下:
// traits/sumpolicy1.hpp #ifndef SUMPOLICY_HPP #define SUMPOLICY_HPP class SumPolicy { public: template<typename T1, typename T2> // 成員模板 static void accumulate(T1& total, T2 const & value) { total += value; } }; #endif //SUMPOLICY_HPP
在這個例子中,我們把policy實現為一個具有一個成員函數模板的普通類(也就是說,類本身不是模板,而且該成員函數是隱式內聯的)。后面我們還會討論另一種實現方案。
通過給累積值指定一個不同的policy,我們就可以進行不同的計算。如下:
// traits/accum7.cpp #include "accum6.hpp" #include <iostream> class MultiPolicy { public: template<typename T1, typename T2> static void accumulate(T1& total, T2 const & value){ total *= value; } }; int main() { // 創建含有具有5個整型值的數組 int num[] = {1, 2, 3, 4, 5}; // 輸出所有值的乘積 std::cout << "the product of the integer values is " << Accum<int, MultiPolicy>::accum(&num[0], &num[5]) << '\n'; } // 程序的輸出結果卻出乎我們意料: the product of the integer values is 0
顯然,這里的問題是我們對初始值的選擇不當所造成的:因為對於求和,0是一個合適的初值;但對於求積,0卻是一個錯誤的初值。可以在policy實現zero()的trait,也可以把這個初值作為參數傳遞進來。
15.1.5 trait和policy:區別在何處
大多數人接受Andrei Alexandrescu在Modern C++ Design中給出的聲明:
policyhe trait具有許多共同點,但是policy更加注重於行為,而trait則更加注重於類型。
另外,作為引入了trait技術的第1人,Nathan Myers給出了下面這個更加開放的定義:
trait class:是一種用於代替模板參數的類。作為一個類,它可以是有用的類型,也可以是常量;作為一個模板,它提供了一種實現“額外層次間接性”的途徑,而正是這種“額外層次間接性”解決了所有的軟件問題。
因此,我們通常會使用下面這些(並不是非常准確的)定義:
(1)trait表述了模板參數的一些自然的額外屬性;
(2)policy表述了泛型函數和泛型類的一些可配置行為(通常都具有被經常使用的缺省值)。
為了更深入地分析這兩個概念之間可能的區別,我們給出下面針對trait的一些事實:
(1)trait可以是fixed trait(也就是說,不需要通過模板參數進行傳遞的trait)。
(2)trait參數通常都具有很自然的缺省值(該缺省值很少會被改寫的,或者是不能被改寫的)。
(3)trait參數可以緊密依賴於一個或多個主參數。
(4)trait通常都是用trait模板來實現的。
對於policy class,我們將會發現下列事實:
(1)如果不以模板參數的形式進行傳遞的話,policy class幾乎不起作用。
(2)policy 參數並不需要具有缺省值,而且通常都是顯式指定這個參數(盡管許多泛型組件都配置了使用頻率很高的缺省policy)。
(3)policy參數和屬於同一個模板的其他模板參數通常都是正交的。
(4)policy class一般都包含了成員函數。
(5)policy既可以用普通類來實現,也可以用類模板來實現。
15.1.6 成員模板和模板的模板參數
為了實現一個累積policy,在前面我們選擇把Sumpolicy和MutPolicy實現為具有成員模板的普通類。另外,還存在另一種實現方法,即使用類模板來設計這個policy class接口,而這個policy class也就被用作模板的模板實參。如下:
// traits/sumpolicy2.hpp #ifndef SUMPOLICY_HPP #define SUMPOLICY_HPP template <typename T1, typename T2> class SumPolicy { public: static void accumulate (T1& total, T2 const & value) { total += value; } }; #endif //SUMPOLICY_HPP
於是,可以對Accum的接口進行修改,從而使用一個模板的模板參數,如下:
// traits/accum8.hpp #ifndef ACCUM_HPP #define ACCUM_HPP #include "accumtraits4.hpp" #include "sumpolicy2.hpp" template <typename T, // 模板的模板參數一般不會在類里面使用到,故而可以匿名 template<typename, typename> class Policy = SumPolicy, typename Traits = AccumulationTraits<T> > class Accum { public: typedef typename Traits::AccT AccT; static AccT accum(T const* beg, T const* end) { AccT total = Traits::zero(); while (beg != end) { Policy<AccT, T>::accumulate(total, *beg); ++beg; } return total; } }; #endif // ACCUM_HPP
我們也可以不把AccT類型顯式地傳遞給policy類型,而是只傳遞上面的累積trait,並且根據這個trait參數來確定返回結果的類型,而且這樣做在某些情況下(諸如需要給trait其他的一些信息)是有利的。
通過模板的模板參數訪問policy class的主要優點在於:借助於某個依賴於模板參數的類型,就可以很容易地讓policy class攜帶一些狀態信息(也就是靜態成員變量)。而在我們的第1種解決方案中,卻不得不把靜態成員變量嵌入到成員類模板中。
然而,這種利用模板的模板參數的解決方案也存在一個缺點:policy類現在必須被寫成模板,而且我們的接口中還定義了模板參數的確切個數。遺憾的是,這個定義會讓我們無法在policy中添加額外的模板參數。例如,我們希望給SumPolicy添加一個Boolean型的非類型模板實參,從而可以選擇是用 += 運算符來進行求和,還是只用 + 運算符來進行求和。在這個例子中,如果我們使用的是前面( traits/accum6.hpp)的成員模板,那么只需要這樣更改SumPolicy模板即可:
// traits/sumpolicy3.hpp #ifndef SUMPOLICY_HPP #define SUMPOLICY_HPP template<bool use_compound_op = true> class SumPolicy { public: template<typename T1, typename T2> // 成員模板 static void accumulate(T1& total, T2 const & value) { total += value; } }; // 模板特化 template<> class SumPolicy<false> { public: template<typename T1, typename T2> // 成員模板 static void accumulate(T1& total, T2 const & value) { total = total + value; } }; #endif //SUMPOLICY_HPP
然而,如果我們使用模板的模板參數來實現上面的Accum,那么將不能做這樣的修改。
15.1.7 組合多個policie和/或 trait
從我們上面的開發過程可以看出,trait和policy通常都不能完全代替多個模板參數;然而,trait和policy確實可以減少模板參數的個數,並把個數限制在可控制的范圍以內。一種簡單的策略就是根據缺省值使用頻率遞增地對各個參數進行排序。顯然,這意味着:trait參數將位於policy參數的后面(即右邊),因為我們在客戶端代碼中通常都會對policy參數進行改寫。
15.1.8 運用普通的迭代器進行累積
這里直接給出代碼,STL源碼中比較多的使用了這個用法:
// traits/accum0.hpp #ifndef ACCUM_HPP #define ACCUM_HPP #include<iterator> template <typename Iter> // 處理普通迭代器 inline typename std::iterator_traits<Iter>::value_type // 迭代器萃取器 accum(Iter start, Iter, end) { typedef typename std::iterator_traits<Iter>::value_type VT; VT total = VT(); // 假設VT()實際上生成了一個0值 while (start != end) { total += *start; ++start; } return total; } #endif // ACCUM_HPP
iterator_trait結構封裝了迭代器的所有相關屬性:
namespace std { template <typename T> struct iterator_traits<T*> { typedef T value_type; typedef ptrdiff_t difference_type; typedef random_access_iterator_tag iterator_category; typedef T* pointer; typedef T& reference; }; }
然而,由於迭代器所引用的類型並不能表示累積值的類型,因此我們仍然需要自己設計AccumulationTraits。
15.2 類型函數
通過前面的trait例子,我們知道可以根據某些類型來定義某種行為。這與我們通常在程序設計中實現是不同的。在C和C++中,更准確而言,函數可以被稱為函數:函數接收的參數是某些值,而且函數的返回結果也是值。現在,我們要說明的是類型函數:一個接收某些類型實參,並且生成一個類型作為函數的返回結果。
sizeof就是一個非常有用的、內建的類型函數,它返回一個描述給定類型實參大小(以字節為單位)的常量。另一方面,類模板也可以作為類型函數。類型函數的參數可以是模板的參數,而結果就是抽取出來的成員類型或成員變量。例如,可以把sizeof運算符改變成下面的接口:
// traits/sizeof.cpp #include <stddef.h> #include <iostream> template <typename T> class TypeSize { public: static size_t const value = sizeof(T); }; int main() { std::cout << "TypeSize<int>::value = " << TypeSize<int>::value << std::endl; // 抽取了int的成員value }
接下來的內容,提供的是一些具有普遍用途的類型函數,而且它們都可以被用作trait類。
15.2.1 確定元素的類型
如下給定容器的類型,確定容器元素的類型:
// traits/elementtype.cpp #include <vector> #include <list> #include <stack> #include <isotream> #include <typeinfo> template <typename T> class ElementT; // 基本模板 template <typename T> class ElementT<std::vector<T> > // 局部特化 { public: typedef T Type; }; template <typename T> class ElementT<std::list<T> > // 局部特化 { public: typedef T Type; }; template <typename T> class ElementT<std::stack<T> > // 局部特化 { public: typedef T Type; }; template <typename T> void print_element_type (T const & c) { std::cout << "Container of " << typeid(typename ElementT<T>::Type).name() << "elements. \n"; } int main() { std::stack<bool> s; print_element_type(s); }
借助於局部特化的這種用法,即使在容器類型並沒有意識到類型函數的情況下,也可以實現這種類型抽取。然而,大多數情況下,類型函數通常是和可應用類型(即這里的容器類型)一起實現的,而且這樣的話,后面的設計通常都可以被簡化。例如,如果容器類型定義了一個成員類型value_type(諸如標准容器的實現一樣),那么我們就可以編寫如下代碼:
// 前面的三個特化可以簡化成如下代碼 template <typename C> class ElementT { public: typedef typename C::value_type Type; };
上面的代碼可以作為一種缺省實現,而且對於沒有定義成員類型的value_type的容器類型,我們還可以進行特化,因為缺省實現和這里的特化是相容的。因此,我們通常建議在容器模板的定義內部,提供模板類型參數的類型定義,從而在泛型代碼中可以更容易地訪問這些參數類型,如下:
template <typename T1, typename T2, ..... > class X { public: typedef T1 ...; typedef T2 ...; .... };
類型函數之所以有用,是因為它使我們能夠根據容器類型來參數化一個模板:從而在使用該模板的時候,我們並不需要給出代表元素類型和其他特征的一些參數。如下:
template <typename T, typename C> T sum_of_elements(C const& c);
上面的代碼要求我們使用諸如sum_of_elements<int>(list)的調用表達式(實參演繹,實參演繹不能用於返回值),也就是說需要顯示指定元素的類型。然而,如果使用如下聲明:
template <typename C> typename ElementT<C>::Type sum_of_elements (C const& c);
那么我們就可以根據類型函數來抽取元素類型。
15.2.2 確定class 類型
運用下面的類型函數,我們能夠確定某個函數是否為class類型:
// traits/isclasst.hpp template<typename T> class IsClassT { private: typedef char One; typedef struct { char a[2]; } Two; template<typename C> static One test(int C::*); //更加特殊,接受 int類型的成員指針 template<typename C> static Two test(...); // 接受任何參數 public: enum { Yes = sizeof(IsClassT<T>::test<T>(0)) == 1 }; enum { No = !Yes}; };
上面的模板使用了SFINAE原則(substitution-failure-is-not-an-error,替換失敗並非錯誤)。這里用到SFINAE原則的目的在於找到這樣的一個類型構造:它對class類型是無效的,而對於其他的類型則是有效地;或者相反。於是,在這里我們可以依賴於下面這個事實:只有當C是一個class類型的時候,身為成員指針類型的類型構造C::*才會是有效的。
如下應用這個類型函數,來測試某個特定類型是否是class類型:
// triats/isclasst.cpp #include <iostream> #include "isclasst.hpp" class MyClass { }; struct MyStruct { }; union MyUnion { }; void myfunc() { } enum E{ e1 }e; // 以模板實參的方式傳遞類型,並對該類型進行檢查 template <typename T> void check() { if (IsClassT<T>::Yes) { std::cout << "IsClassT " << std::endl; }else { std::cout << " !IsClassT " << std::endl; } } // 以函數調用實參的方式傳遞類型,並對該類型進行檢查 template <typename T> void checkT(T) { check<T>(); } int main() { std::cout << " int: "; check<int>(); std::cout << " MyClass: "; check<MyClass>(); .... } 輸出: int: !IsClassT MyClass: IsClassT
15.2.3 引用和限定符
考慮如下函數模板定義:
// traits/apply1.hpp template <typename T> void apply(T& arg, void (*func)(T)) { func(arg); }
同時考慮下面使用代碼:
// traits/apply1.cpp #include<iostream> #include "apply1.hpp" void incr(int& a) { ++a; } void print(int a) { std::cout << a << std::endl; } int main() { int x = 7; apply(x, print); // (1) apply(x, incr); // (2) }
(1)的調用正確:用int來替換T。
然而(2)的調用,如果匹配第2個參數,那么要求用int&來替換T,而這意味着第1個參數類型為int&&,但int&&通常都不是合法的C++類型。
借助局部特化,我們可以如下解決這個問題:
// traits/typeop1.hpp template <typename T> class TypeOp // 基本模板 { public: typedef T ArgT; typedef T BareT; typedef T const ConstT; //添加const限定 typedef T & RefT; typedef T & RefBareT; typedef T const & RefConstT; };
首先,我們可以實現一個處理const類型的局部特化
// traits/typeop2.hpp template <typename T> class TypeOp<T const> // 針對const類型的局部特化 { public: typedef T const ArgT; typedef T BareT; // 去除const限定 typedef T const ConstT; typedef T const & RefT; typedef T & RefBareT; // 添加&引用 typedef T const & RefConstT; };
針對引用類型的局部特化同樣也適用於reference-to-const類型:
// traits/typeop3.hpp template <typename T> class TypeOp<T&> // 針對引用的局部特化 { public: typedef T & ArgT; typedef typename TypeOp<T>::BareT BareT; // 引用的基本類型 typedef T const ConstT; typedef T & RefT; typedef typename TypeOp<T>::BareT & RefBareT; typedef T const & RefConstT; };
特殊情況:指向void的引用是不允許的:
// traits/typeop3.hpp template <> class TypeOp<void> // 針對void的全局特化 { public: typedef void ArgT; typedef void BareT; typedef void const ConstT; typedef void RefT; typedef void RefBareT; typedef void RefConstT; };
有了上面這幾部分代碼,我們就可以改寫apply模板如下:
// 現在只能根據第2個實參來演繹T了,因為T位於一個受限名稱中 template <typename T> void apply(typename TypeOp<T>::RefT arg, void (*func)(T)) { func(arg); }
15.2.4 promotion trait
我們已經研究並且開發了單一類型的類型函數:即給定一個類型,我們可以定義其他相關的類型或者參數。然而,我們通常都需要開發依賴於多個實參的類型參數。一個典型例子就是promotion trait,它在編寫運算符模板的時候非常有用。
假設,我們需要對兩個Array容器進行相加:
template <typename T> Array<T> operator+ (Array<T> const&, Array<T> const &);
這看起來非常好。但是,由於語言允許我們把一個char類型的值加到一個int值,因此,我們期望可以對數組也實現這種混合類型的操作。於是,我們將面臨一個問題,即如何確定結果模板的返回類型。
template <typename T1, typename T2> Array<????> operator+ (Array<T1> const&, Array<T2> const &);
然而,借助於promotion trait,我們就可以解決上面聲明所給出的問題。如下:
//method1 template<typename T1, typename T2> Array<typename Promotion<T1, T2>::ResultT> operator+ (Array<T1> const&, Array<T2> const&); //或者另一種實現方法method2 template<typename T1, typename T2> typename Promotion<Array<T1>, Array<T2> >::ResultT operator+ (Array<T1> const&, Array<T2> const&);
上面的代碼的主要的想法是:提供模板Promotion的一系列特化,從而能夠根據要求生成一個滿足我們需要的類型函數。
另一個使用promotion trait的應用程序是由max()模板引入的;當我們希望指定兩個不同類型值的最大值時,我們通常都期望返回結果(即最大值)屬於“兩個類型中更加強大的類型”,而這個時候往往就會用到類型函數。
實際上,對於Promotion模板,並不存在確切的定義;因此,我們最好是讓這個基本模板處於未定義狀態:
template <typename T1, typename T2>
class Promotion;
另外,如果兩個類型的大小不一樣,那么我們還需要作出另一個選擇:我們將提升類型更強大的類型。我們可以通過特殊模板IfThenElse來實現這一點,它會接受一個Boolean的非類型模板參數,然后根據Boolean參數的值,在兩個類型參數之中選出其中一個:
// traits/ifthenelse.hpp #ifndef IFTHENELSE_HPP #define IFTHENELSE_HPP // 基本模板:根據第1個實參來決定:是選擇第2個實參,還是第3個實參 template <bool C, typename Ta, typename Tb> class IfThenElse; // 局部特化:true的話則選擇第2個實參 template<typename Ta, typename Tb> class IfThenElse<true, Ta, Tb> { public: typedef Ta ResultT; }; // 局部特化:false的話則選擇第3個實參 template<typename Ta, typename Tb> class IfThenElse<false, Ta, Tb> { public: typedef Tb ResultT; }; #endif // IFTHENELSE_HPP
有了上面的這些代碼之后,我們能夠根據所需要提升的類型的大小,從而在T1、T2、void三者之間做出選擇,並且實現Promotion模板如下:
// traits/promote1.hpp // 針對類型提升(type promotion)的基本模板 template<typename T1, typename T2> class Promotion { public: typedef typename IfThenElse<(sizeof(T1) > sizeof(T2), T1, typename IfThenElse<(sizeof(T1) < sizeof(T2)), T2, void >::ResultT >::ResultT ResultT; };
對於在基本模板中使用的這種基於類型大小的啟發式假設,在大多數情況下都可以正常運行;但我們需要對這種假設進行檢驗;而這有時候也是比較麻煩的。另外,如果這種假設選擇了一個錯誤的(即不符合期望的)類型,那么我們還需要給出一個相應的特化,來改寫原來這種(基於假設的)選擇。另一方面,如果兩個類型是完全一樣的,那么馬上就可以安全地把該(相同的)類型提升為所期望的類型。可以用下面的局部特化來闡述這一點:
// traits/promote2.hpp // 針對兩個相同類型的局部特化 template<typename T> class Promotion<T, T> { public: typedef T ResultT; };
為了記錄基本類型的提升,我們還需要實現一系列針對基本類型的特化。在此,可以借助宏來(從某種程度地)減少源代碼的數量:
// traits/promote3.hpp #define MK_PROMOTION(T1, T2, Tr) \ template<> class Promotion<T1, T2> { \ public: \ typedef Tr ResultT; \ }; \ \ template<> class Promotion<T2, T1> { \ public: \ typedef Tr ResultT; \ };
於是,我們可以這樣添加這些提升:
// traits/promote4.hpp MK_PROMOTION(bool, char, int) MK_PROMOTION(bool, unsigned char, int) MK_PROMOTION(bool, signed char, int) .......
一旦為基本類型(和一些必要的枚舉類型)定義好了Promotion,我們就可以通過局部特化來表達其他的提升規則。如Array數組:
// traits/promotearray.hpp // 特化1 template<typename T1, typename T2> class Promotion<Array<T1>, Array<T2> > { public: typedef Array<typename Promotion<T1, T2>::ResultT> ResultT; }; // 特化2 template<typename T, typename T> class Promotion<Array<T>, Array<T> > { public: typedef Array<typename Promotion<T, T>::ResultT> ResultT; };
對於最后一個局部特化,我們需要給予更大的關注。我們剛開始可能會認為前面針對相同類型的特化(Promotion<T, T>)已經考慮了這種情況。然而遺憾的是,就特化程度而言,局部特化Promotion<Array<T1>, Array<T2> > 和局部特化Promotion<T, T>是一樣的。為了避免產生這種(由於特化程度相同而引起的)模板選擇二義性,我們添加了最后一個局部特化,它比前面兩個模板中的任何一個都更加特殊化。
15.3 policy trait
到目前為止,我們給出了幾個trait模板的例子,用於確定模板參數的一些屬性:譬如這些參數表示的是什么類型;在混合類型的操作中,應該提升哪一個類型等等。我們把這些trait稱為property trait。 另一方面,還存在其他類型的trait,它們定義了應該如何對待這些類型,我們把這類trait稱為policy trait。盡管我們通常可以把property trait實現為類型函數,但是對於policy trait而言,我們通常是把該policy封裝在成員函數內部。
15.3.1 只讀的參數類型
在C和C++中,函數調用實參在缺省情況下都是以“傳值”的方式進行傳遞的。但對於很大的數據結構應該采用“傳const引用”。特別的,當引入模板之后,由於我們事先並不知道用來替換模板的參數類型究竟有多大;而且,最后的決定也不僅僅依賴於類型的大小:一個小的結構也可能會具有昂貴的拷貝構造函數。所以,選擇一個適當的傳遞機制變得更加復雜。
在前面的討論中,我們已經隱約提到,可以使用policy trait模板來處理上面這個問題,而且該policy trait實際上是一個類型函數:該函數可以根據不同的情況(即類型大小),將把實參類型T映射為T或者T const&,即在這兩種類型中挑選一種最佳參數類型。基於下面的例子,我們做出一個近似的假設:對於不大於“2個指針”大小的類型,基本模板將采用“傳值”的方式傳遞參數,而對於其他的類型,則采用“傳遞const引用”的方式傳遞參數。
template<typename T> class RParam { public: typedef typename IfThenElse<sizeof(T) <= 2*sizeof(void*), T, T const&>::ResultT Type; };
另一方面,對於容器類型,即使sizeof函數返回的是一個很小的值,但也可能會涉及到昂貴的拷貝構造函數。因此,我們需要編寫如下(針對Array)的許多特化和局部特化:
template<typename T> class RParam<Array<T> > { public: typedef Array<T> const& Type; };
由於我們處理的都是C++中的常見類型,所以我們期望在基本模板中能夠對非class類型以傳值的方式進行調用。另外對於某些對性能要求比較苛刻的class類型,我們有選擇地添加這些類為“傳值”方式。
template<typename T> class RParam { public: // class類型,使用傳const引用 typedef typename IfThenElse<IsClassT<T>::No, T, T const&>::ResultT Type; }; // 特化 : 針對RParam<>的MyClass2參數,以傳值的方式進行傳遞 template<> class RParam<MyClass2> { public: typedef MyClass2 Type; };
15.3.2 拷貝、交換和移動
書中引入了一個policy trait模板,它將選擇出最佳操作,來拷貝、交換或者移動某一特定類型的元素。