C++ 模板型別萃取技術 traits
當函式,類或者一些封裝的通用演算法中的某些部分會因為資料型別不同而導致處理或邏輯不同(而我們又不希望因為資料型別的差異而修改演算法本身的封裝時),traits會是一種很好的解決方案。(型別測試發生在編譯期)
自從C++中引入了template後,以泛型技術為中心的設計得到了長足的進步。STL就是這個階段傑出的產物。STL的目標就是要把資料和演算法分開,分別對其進行設計,之後通過一種名為iterator的東西,把這二者再粘接到一起。設計模式中,關於iterator的描述為:一種能夠順序訪問容器中每個元素的方法,使用該方法不能暴露容器內部的表達方式。可以說,型別萃取技術就是為了要解決和iterator有關的問題的,下面,我們就來看看整個故事。
應該說,迭代器就是一種智慧指標,因此,它也就擁有了一般指標的所有特點——能夠對其進行*和->操作。但是在遍歷容器的時候,不可避免的要對遍歷的容器內部有所瞭解,所以,設計一個迭代器也就自然而然的變成了資料結構開發者的一個義務,而這些iterators的表現都是一樣的,這種內外的差異,對使用者來說,是完全透明的,
第一部分 為什麼要有萃取技術
既然是一種智慧指標,iterator也要對一個原生指標進行封裝,而問題就源於此,當我們需要這個原生指標所指物件的型別的時候(例如宣告變數),怎麼辦呢?
Case1 對於函式的區域性變數
這種情況我們可以採用模版的引數推導,例如:
template <class T> void func(T iter)
如果T是某個指向特定物件的指標,那麼在func中需要指標所指向物件型別的變數的時候,怎麼辦呢?這個還比較容易,模板的引數推導機制可以完成任務,如下:
template <class T, class U> void func_impl(T t, U u) { U temp; // OK, we’ve got the type // The rest work of func… } template <class T> void func(T t) { func_impl(t, *t); // forward the task to func_impl }
通過模板的推導機制,我們輕而易舉的或得了指標所指向的物件的型別,但是事情往往不那麼簡單。例如,如果我想把傳遞給func的這個指標引數所指的型別作為返回值,顯然這個方法不能湊效了,這就是我們的case 2。
Case2 對於函式的返回值
儘管在func_impl中我們可以把U作為函式的返回值,但是問題是使用者需要呼叫的是func,於是,你不可能寫出下面的程式碼:
template <class T, class U>
U func_impl(T t, U u) {
U temp; // OK, we’ve got the type
// The rest work of func…
}
template <class T>
(*T) func(T t) { // !!!Wrong code
return func_impl(t, *t); // forward the task to func_impl
}
int i =10;
cout<<func(&i)<<endl; // !!! Can’t pass compile
(*T)概念上如此正確,目的在於解引用迭代器,不過所有的編譯器都會讓你失望。這個問題解決起來也不難,只要做一個iterator,然後在定義的時候為其指向的物件型別制定一個別名,就好了,像下面這樣:
template <class T>
struct MyIter {
typedef T value_type; // A nested type declaration, important!!!
T* ptr;
MyIter(T* p = 0) : ptr(p) {}
T& operator*() const { return *ptr; }
};
而後只要需要其指向的物件的型別,只要直接引用就好了,例如:
template <class I>
typename I::value_type func(I iter) { return *iter; }
很漂亮的解決方案,看上去一切都很完美。但是,實際上還是有問題,因為func如果是一個泛型演算法,那麼它也絕對要接受一個原生指標作為迭代器,但是顯然,你無法讓下面的程式碼編譯通過:
int *p = new int(52);
cout<<func(p)<<endl; // !!!Is there a int::value_type?? Wrong Code here
我們的func無法支援原生指標,這顯然是不能接受的。此時,template partial specialization就派上了用場。
Solution:template partial specialization是救世主
既然剛才的設計方案仍不完美,那我們就再加一個間接層,把智慧指標和原生指標統統的封裝起來。在討論之前,先要澄清一下template partial specialization的含義。所謂的partial specialization和模板的預設引數是完全不同的兩件事情,前者指的是當引數為某一類特定型別的時候,採用特殊的設計,也就是說是“針對template引數更進一步的條件限制所設計出來的一個特化版本”;而預設引數則是當不提供某些引數的時候,使用的一個預設。
參考:partial specialization的語法
Template <typename T> class C<T*> {} // 為所有型別為T*的引數而準備的特殊版本
好了,下面我們就找一個專職的負責人,用來封裝迭代器封裝的物件型別。首先,把我們剛才的MyIter重新包裝一下:
template <class I>
struct iterator_traits {
Typedef I::value_type value_type;
}
現在,我們的func又有了新的面貌:
template <class I>
typename iterator_traits<I>::value_type func(I ite) {
return *ite;
}
儘管這次我們的函式返回值的長度有些嚇人,但是,我們的確為原生指標找到了好的解決方案。只要為原生指標提供一個偏特化的iterator_traits就OK了。如下:
template <class I>
struct iterator_traits<T*> {
typedef T value_type;
};
下面,我們終於可以讓func同時支援智慧指標和原生指標了:
template <class I>
struct iterator_traits {
Typedef I::value_type value_type;
}
template <class T>
struct iterator_traits<T*> {
typedef T value_type;
};
template <class I>
typename iterator_traits<I>::value_type func(I ite) {
return *ite;
}
int main() {
MyIter<int> iter = new int(520);
int *p = new int(520);
// This time the following two statements will success
cout<<func(iter)<<endl;
cout<<func(p)<<endl;
return 0;
}
但是,我們離最後的成功還有最後一步,如果,我們需要宣告一個value_type型別的左值,但是卻給iterator_traits傳遞了一個const int*,顯然結果有問題,於是,為const T*也另起爐灶,準備一份小炒:
template<class T>
struct iterator_traits<const T*> {
typedef T value_type;
}
OK,現在萬事大吉,無論是正宗迭代器,原生指標,const原生指標,我們都可以利用iterator_traits萃取出其封裝的物件的型別,萃取技術由此而來。
第二部分 基於泛型的型別萃取技術
總結一下,我們之所以要萃取迭代器相關的型別,無非是要把迭代器相關的型別用於宣告區域性變數、用作函式的返回值等一系列行為。對於原生指標和point-to-const型別的指標,採用模板偏特化技術對其進行特殊處理,另外,對於point-to-const型別的指標,為了保證宣告左值時語義正確,特化時按照普通原生指標處理。
實際上,把我們剛才的例子提煉一下,迭代器相應型別不僅僅有迭代器封裝的物件型別,STL中對這些型別作了整理,有如下幾種:
template <class I>
struct iterator_traits {
typedef typename I::iterator_category iterator_category;
typedef typename I::value_type value_type;
typedef typename I::difference_type difference_type;
typedef typename I::pointer pointer;
typedef typename I::reference reference;
}
當然,也如你所想,對於原生pointer和pointer-to-const這兩種情況,STL分別對其進行了特化處理。如果你看了上面的程式碼卻不知所云,也屬正常,在去了解特化版本之前,我們先來看看這五種型別的含義。
Type 1 value_type
這個型別和我們在第一部分談到的vlaue_type的含義是一樣的,不多說了。
Type 2 difference_type
用來表示兩個迭代器之間的最大距離。這個型別用來對某種演算法提供計數功能,例如:
template <class I, class T>
typename iterator_traits<I>::difference_type
count(I first, I last, const T& value){
typename iterator_traits<I>::difference_type n = 0;
for(; first != last; first++) {
if(*first == value)
n++;
}
return n;
}
也許這個例子最足以說明問題,任何的解釋都沒必要了。這裡需要說明的是。對於原生指標,由於不存在int::difference_type的情況,所以,iterator_traits對其進行特化:
template <class I>
class iterator_traits<I*> {
typedef ptrdiff_t difference_type;
}
這裡,ptrdiff_t是定義在cstddef中的一個C++內建型別,在GNU gcc中,定義如下:
typedef long int ptrdiff_t;
同樣,對於pointer-to-const,也要入法炮製:
template <class I>
class iterator_traits<const I*> {
typedef ptrdiff_t difference_type;
}
再一次,偏特化技術幫了大忙,現在count可以處理所有型別迭代器的difference_type了。
Type 3 reference
這裡,reference type指的是迭代器封裝物件的型別的引用。這個型別的出現主要是為了解決對指標進行解引用的時候,返回什麼樣的物件的問題。我們希望:
MyIter<int> iter(new int(10));
*iter = 52;
和
Int *p = new int(10);
*p = 52;
是一樣的。於是,reference_type一般用在迭代器的*運算子過載上,讓所有的“指標家族”有同樣的表現形式。於是,如果value_type是T,那麼reference_type就是T&,如果value_type是const T,reference_type就是const T&。
Type 4 pointer
C++中指標和引用總是有著密切的關係。如果我們想返回迭代器封裝的物件的地址,就需要用到這裡的pointer_type,主要用在迭代器中對->運算子過載的問題。對於一個智慧指標來說,通常我們都需要下面的兩個運算子過載:
T& operator*() const { return *ptr; } // T& is reference type
T* operator->() const { return ptr; } // T* is pointer type
同樣,為了能夠對迭代器和原生指標都能夠在演算法上有統一的表現形式,在iterator_traits中加入了下面的型別
template <class T>
struct iterator_traits {
typedef typename I::pointer pointer;
typedef typename I::reference reference;
}
同樣,對於原生指標和point-to-const型別的指標作了特化:
template<class T>
struct iterator_traits<T*> {
typedef typename T* pointer;
typedef typename T& reference;
}
而這次,對於point-to-const型別的指標,則有些特別:
template<class T>
struct iterator_traits<const T*> {
typedef typename const T* pointer;
typedef typename const T& reference;
}
也就是說,當我們解引用一個封裝了常量物件的迭代器的時候,返回的型別應該是const T&,取一個封裝了常量對物件的迭代器中的元素的地址,返回的應該是const T*。最終的結果,就是所有的演算法都有了一個統一的表達方式:
template <class T>
typename iterator_traits<T>::reference func() {}
template <class T>
typename iterator_traits<T>::pointer func() {}
Type 5 iterator_category
這個型別的作用是按照迭代器的移動特性和能夠在該迭代器上實施的操作對迭代器進行分類,之所以這樣做,完全是為了效率的考量。不過,在我看來,對其分類的因素實際上只有迭代器的移動特性,而分類,也非常簡單:一步步向前挪的型別和一步跨到位的型別。
在STL中,共有以下5種迭代器型別:
l 單向移動只讀迭代器 Input Iterator
l 單向移動只寫迭代器 Output Iterator
l 單向移動讀寫迭代器 Forward Iterator
l 雙向移動讀寫迭代器 Bidirectional Iterator
以上4種屬於單步向前挪型的迭代器,還有一種雙向移動讀寫迭代器屬於一步跨到位型:
l 隨機訪問迭代器 Random Access Iterator
按照強化關係,上面5種迭代器的關係如下:
Input Iterator Output Iterator
| |
+-----------+----------+
|
Forward Iterator
|
Bidirectional Iterator
|
Random Access Iterator
在STL的各種演算法中,遍歷元素是很常用的,於是我們就用advance()這個函式作個例子,看看每個迭代器的型別,這個函式負責把迭代器移動特定的長度:
// The input iterator version, an O(N) algorithm
template <class InputIterator, class Distance>
void Advance_II(InputIteraotr& i, Distance n) {
while(n--) i++; // This is step by step moving
}
其實,Output和Forward型別的迭代器在移動上和Input型別是一樣的。不再熬述,來看看Bidirectional型別:
// The bidirectional iterator version, an O(N) algorithm
template <class BidirectionalIterator, class Distance>
void Advance_BI(BidirectionalIterator& i, Distance n) {
if(n >= 0)
while(n--) i++;
else
while(n++) i++;
}
加入了雙向移動,但仍然要單步進行。最後,看看隨機訪問型別:
// The random access version, an O(1) algorithm
template <class RandomAccessIterator, class Distance>
void Advance_RAI(RandomAccessIterator& i, Distance n) {
i += n;
}
最後,我們可以構想一個把這3個函式封裝起來的函式advance,專門負責迭代器的移動。
template <class InputIterator, class Distance>
void advance(InputIterator& I, Distance n) {
if(is_ramdom_access_iterator(i)) // How to judge?
advance_RAI(I, i);
else if(is_bidirectional_iterator(i)) // How to judge?
Advance_BI(I, i);
else
Advance_II(I, i);
}
但是,在程式執行時決定函式呼叫,顯然效率不彰,最好能夠讓編譯器在程式編譯的時候決定函式呼叫,於是,我們要想方設法利用函式過載,讓編譯器幫助我們決策函式呼叫。這樣,就需要我們對於迭代器的型別做一個統一的規劃,OO正好能幫助我們解決這個問題,設計下面的繼承結構,這和我們上面畫的那張圖是一樣的:
// five tag classes
struct input_iterator_tag { }
struct output_iterator_tag { }
struct forward_iterator_tag : public input_iterator_tag { }
struct bidirectional_iterator_tag : public forward_iterator_tag { }
struct random_access_tag : public bidirectional_iterator_tag { }
之後,重新設計__advance,給它加上第3個引數——用以表明此迭代器型別的標籤,根據此標籤來決定不同的__advance操作(此時,type_traits技術派上了用場)。而對外開放的advance仍然不變:
template <class InputIterator, class Distance>
void advance(InputIterator& i, Distance n) {
// Forward the correct messages
__advance(i, n, type_traits<i>::iterator_category());
}
說到這裡,你也就應該明白iterator_category的作用了,同樣,為poiner準備了兩個特化版本:
template <class T>
struct iterator_traits<T*> {
typedef random_access_iterator_tag iterator_category;
}
template <class T>
struct iterator_traits<const T*> {
typedef random_access_iterator_tag iterator_category;
}
道理很簡單,所有的原生指標都支援隨機訪問。
第三部分 雜項
在STL中,所有的迭代器都遵從上面的設計原則,都要提供上面說過的五種型別,但是,人總會有掛一漏萬的時候,為了設計上的方便,STL提供了一個標準的迭代器殼:
template <class Category,
class T,
class Distance = ptrdiff_t
class Pointer = T*
class Reference = T&>
struct iterator {
typedef Category iterator_category;
typedef T value_type;
typedef Distance difference_type;
typedef Pointer pointer;
typedef Reference reference;
};
這樣就免去了宣告這些型別的麻煩,當你想自定義一個迭代器的時候:
template <class Item>
struct MyIter : public std::iterator<std::forward_iterator_tag, Item> { … }
就萬事大吉了。
/////////////////////////////////////////////////////////////////////////////////////////////////
Traits技術可以用來獲得一個 型別 的相關資訊的。 首先假如有以下一個泛型的迭代器類,其中型別引數 T 為迭代器所指向的型別:
template <typename T>
class myIterator
{
...
};
當我們使用myIterator時,怎樣才能獲知它所指向的元素的型別呢?我們可以為這個類加入一個內嵌型別,像這樣:
template <typename T>
class myIterator
{
typedef T value_type;
...
};
這樣當我們使用myIterator型別時,可以通過 myIterator::value_type來獲得相應的myIterator所指向的型別。
現在我們來設計一個演算法,使用這個資訊。
template <typename T>
typename myIterator<T>::value_type Foo(myIterator<T> i)
{
...
}
這裡我們定義了一個函式Foo,它的返回為為 引數i 所指向的型別,也就是T,那麼我們為什麼還要興師動眾的使用那個value_type呢? 那是因為,當我們希望修改Foo函式,使它能夠適應所有型別的迭代器時,我們可以這樣寫:
template <typename I> //這裡的I可以是任意型別的迭代器
typename I::value_type Foo(I i)
{
...
}
現在,任意定義了 value_type內嵌型別的迭代器都可以做為Foo的引數了,並且Foo的返回值的型別將與相應迭代器所指的元素的型別一致。至此一切問題似乎都已解決,我們並沒有使用任何特殊的技術。然而當考慮到以下情況時,新的問題便顯現出來了:
原生指標也完全可以做為迭代器來使用,然而我們顯然沒有辦法為原生指標新增一個value_type的內嵌型別,如此一來我們的Foo()函式就不能適用原生指標了,這不能不說是一大缺憾。那麼有什麼辦法可以解決這個問題呢? 此時便是我們的主角:型別資訊榨取機 Traits 登場的時候了
....drum roll......
我們可以不直接使用myIterator的value_type,而是通過另一個類來把這個資訊提取出來:
template <typename T>
class Traits
{
typedef typename T::value_type value_type;
};
這樣,我們可以通過 Traits<myIterator>::value_type 來獲得myIterator的value_type,於是我們把Foo函式改寫成:
template <typename I> //這裡的I可以是任意型別的迭代器
typename Traits<I>::value_type Foo(I i)
{
...
}
然而,即使這樣,那個原生指標的問題仍然沒有解決,因為Trait類一樣沒辦法獲得原生指標的相關資訊。於是我們祭出C++的又一件利器--偏特化(partial specialization):
template <typename T>
class Traits<T*> //注意 這裡針對原生指標進行了偏特化
{
typedef typename T value_type;
};
通過上面這個 Traits的偏特化版本,我們陳述了這樣一個事實:一個 T* 型別的指標所指向的元素的型別為 T。
如此一來,我們的 Foo函式就完全可以適用於原生指標了。比如:
int * p;
....
int i = Foo(p);
Traits會自動推匯出 p 所指元素的型別為 int,從而Foo正確返回。