STL主要由為容器,迭代器和算法創建的模板組成,但是也有一些功能模板。其中之一叫做advance。Advance將一個指定的迭代器移動指定的距離:
1 template<typename IterT, typename DistT> // move iter d units 2 void advance(IterT& iter, DistT d); // forward; if d < 0, 3 // move iter backward
從概念上來說,advance僅僅做了iter += d,但是advance並不是用這種方式實現的,因為只有隨機訪問迭代器支持+=操作。其他一些更加弱的迭代器類型必須通過反復的++或者--d次來實現advance。
你不記得STL迭代器的種類了?沒問題,我們現在做一些簡單的回顧。總共有5類迭代器,對應著它們支持的五種操作。輸入迭代器(input iterator)只能向前移動,一次只能移動一步,只能讀取它們指向的內容,並且只能讀一次。這種迭代器模擬的是輸入文件的讀指針;C++庫的istream_iterator是這種迭代器的代表。輸出迭代器(output iterator)同輸入迭代器類似,但是對於輸出迭代器來說:它們只能向前移動,每次只能移動一步,只能對它們指向的內存進行寫操作,並且只能寫一次。它們模擬的是輸出文件的寫指針;ostream_iterator代表了這種迭代器。這是兩類功能最弱的迭代器。因為輸入和輸出迭代器只能向前移動,只能對它們所指向的內容進行讀寫最多一次,它們只能為one-pass算法所使用。
一個更加強大的迭代器種類由前向迭代器(forward iterator)組成。這種迭代器能做輸入和輸出迭代器能做的所有事情,並且它們能對它們指向的內容進行多次的讀寫。這就使得它們能被multi-pass算法所使用。STL沒有提供單鏈表,但是一些庫卻提供了(通常叫做slist),這種容器中的迭代器為前向迭代器。TR1中的哈希容器(Item 54)迭代器也可能是前向迭代器。
雙向迭代器(bidirectional iterators)和前向迭代器相比添加了向後移動的能力。為STL中的list提供的迭代器就屬於這種類別,為set,multiset,map和multimap提供的迭代器也是這種類別。
最強大的迭代器類別叫做隨機訪問迭代器(random access iterator)。這種類型的迭代器和雙向迭代器相比添加了執行“迭代器運算(iterator arithmetic)”的能力,也就是在常量時間內向前或者向後跳躍任意的距離。這種運算同指針運算類似,不要吃驚,因為隨機訪問迭代器模擬的就是內建類型的指針,內建類型指針的行為表現就如同隨機訪問迭代器。Vector,deque和string迭代器都是隨機訪問迭代器。
為了識別這五種迭代器類型,C++在標准庫中為五種迭代器類型提供了一個“tag結構體”:
1 struct input_iterator_tag {}; 2 struct output_iterator_tag {}; 3 struct forward_iterator_tag: public input_iterator_tag {}; 4 struct bidirectional_iterator_tag: public forward_iterator_tag {}; 5 struct random_access_iterator_tag: public bidirectional_iterator_tag {};
這些結構體之間的繼承關系是有效的“is-a”關系(Item32):所有的前向迭代器同樣是輸入迭代器,等等。我們很快會看到這種繼承的效用。
回到advance。考慮到不同的迭代器功能,實現advance的一種方法是使用循環的最小公分母策略:對迭代器進行反復加或者減。然而,這個方法會花費線性的時間。隨機訪問迭代器支持常量時間的迭代器算法,在我們需要的時候會使用它的這種能力。
我們真正想要的是像下面這樣去實現advance:
1 template<typename IterT, typename DistT> 2 void advance(IterT& iter, DistT d) 3 { 4 if (iter is a random access iterator) { 5 6 iter += d; // use iterator arithmetic 7 8 } // for random access iters 9 10 else { 11 12 13 if (d >= 0) { while (d--) ++iter; } // use iterative calls to 14 else { while (d++) --iter; } // ++ or -- for other 15 } // iterator categories 16 }
這需要決定iter是不是一個隨機訪問迭代器,也就是需要知道它的類型,IterT,是不是隨機訪問迭代器類型。換句話說,我們需要獲取一個類型的相關信息。這也是trait讓你所做的:它們允許你在編譯期間獲取一個類型的相關信息。
Traits不是C++中的關鍵字或者一個預定義的概念;它們是一種技術,也是一個C++程序員需要遵守的約定。使用這項技術的一個要求是它必須使內建類型同用戶自定義類型一樣能夠很好的工作。例如,如果advance的入參為一個指針(像const char*)和一個Int,advance必須能夠工作,但是這就意味著trait技術必須能夠使用在像指針一樣的內建類型上。
Traits必須能夠同內建類型一塊工作就意味著不能在類型內部嵌入一些信息,因為沒有方法在指針內部嵌入信息。於是對於一種類型的traits信息,必須是放在類型外部的。標准的技術是將其放在模板和模板的一個或多個特化實例中。對於迭代器來說,標准庫中的模板被命名為iterator_traits:
1 template<typename IterT> // template for information about 2 struct iterator_traits; // iterator types
正如你所見的,iterator_traits是一個結構體。按照慣例,traits經常被實現為一個結構體。另外一種常用手法是將實現traits的結構體替換為traits class(這不是我說的)。
Iterator_traits的工作方式是對於每個類型IterT,在結構體iterator_traits<IterT>中聲明一個叫做iterator_category的typedef。這個typedef唯一確認了IterT的迭代器類別。
Iterator_traits會在兩部分中實現它。首先,它強制任何用戶自定義的迭代器類型必須包含一個叫做iterator_category的內嵌typedef,它能夠識別合適的tag結構體。舉個例子,deque的迭代器是隨機訪問的,因此一個deque迭代器的類會像是下面這個樣子:
1 template < ... > // template params elided 2 3 class deque { 4 5 public: 6 7 class iterator { 8 9 public: 10 11 typedef random_access_iterator_tag iterator_category; 12 13 ... 14 15 }; 16 17 ... 18 19 };
List的迭代器是雙向的,所以用下面的方式處理:
1 template < ... > 2 3 class list { 4 5 public: 6 7 class iterator { 8 9 public: 10 11 typedef bidirectional_iterator_tag iterator_category; 12 13 ... 14 15 }; 16 17 ... 18 19 };
iterator_traits只是重復使用iterator類的內嵌typedef:
1 // the iterator_category for type IterT is whatever IterT says it is; 2 3 // see Item 42 for info on the use of “typedef typename” 4 5 template<typename IterT> 6 7 struct iterator_traits { 8 9 typedef typename IterT::iterator_category iterator_category; 10 11 ... 12 13 };
這對用戶自定義類型來說會工作的很好,但是對於指針迭代器來說就不工作了,因為指針中沒有內嵌的typedef。Iterator_trait實現的第二部分需要處理指針迭代器。
為了支持這種迭代器,iterator_traits為指針類型提供了一種部分模板特化(partial template specialization)。指針的行為表現同隨機訪問迭代器類似,所以iterator_trait為它們指定了隨機訪問類別:
1 template<typename T> // partial template specialization 2 struct iterator_traits<T*> // for built-in pointer types 3 { 4 typedef random_access_iterator_tag iterator_category; 5 ... 6 };
到現在你應該了解了如何設計和實現一個traits class:
考慮iterator_traits——實際上是std::iterator_traits,既然它是C++標准庫的一部分——我們能為advance的實現精煉成我們自己的偽代碼:
1 template<typename IterT, typename DistT> 2 void advance(IterT& iter, DistT d) 3 { 4 if (typeid(typename std::iterator_traits<IterT>::iterator_category) == 5 typeid(std::random_access_iterator_tag)) 6 ... 7 }
雖然這看上去很有希望,但它不會如我們所願。第一,它會導致編譯問題,這個問題我們將在Item 48研究;現在,有更加基本的問題需要考慮。IterT的類型在編譯期被確認,所以iterator_traits<IterT>::iterator_category同樣能夠在編譯期被確定。但是if語句會在運行時被評估(除非你的優化器足夠瘋狂,把if語句去掉)。為什麼將本來可以在編譯期做的事情挪到運行時去做呢?它會浪費時間,並且會造成執行代碼膨脹。
我們真正想要的是為類型提供一個能夠在編譯期進行評估的條件結構(也就是一個if…else語句)。C++已經有一種方法來實現這種行為。她叫做重載。
當你重載某個函數f的時候,你為不同的重載函數指定不同的參數類型。當你調用f時,編譯器會根據你所傳遞的參數選擇最佳匹配重載函數。編譯器會說:“如果這個重載對於傳遞過來的參數來說是最佳匹配,那麼調用這個f;如果另外一個重載函數是最佳匹配,那麼調用另外一個函數;如果第三個函數是最佳匹配,調用第三個”等等,看到了麼?這是一個與類型相關的編譯期條件結構。為了讓advance表現出我們想要的行為,所有我們必須要做的是創建一個重載函數的多個版本,它們包含了advance的“內髒”,每個函數都帶有一個不同類型的iterator_category對象。我將這些函數命名為doAdvance:
1 template<typename IterT, typename DistT> // use this impl for 2 void doAdvance(IterT& iter, DistT d, // random access 3 std::random_access_iterator_tag) // iterators 4 { 5 iter += d; 6 } 7 template<typename IterT, typename DistT> // use this impl for 8 void doAdvance(IterT& iter, DistT d, // bidirectional 9 std::bidirectional_iterator_tag) // iterators 10 { 11 if (d >= 0) { while (d--) ++iter; } 12 else { while (d++) --iter; } 13 } 14 template<typename IterT, typename DistT> // use this impl for 15 void doAdvance(IterT& iter, DistT d, // input iterators 16 std::input_iterator_tag) 17 { 18 if (d < 0 ) { 19 throw std::out_of_range("Negative distance"); // see below 20 } 21 while (d--) ++iter; 22 }
因為forward_iterator_tag繼承自input_iterator_tag,為input_iterator_tag提供的doAdvance版本同樣能夠處理forward迭代器。這是在不同iterator_tag結構體之間引入繼承的動機。(事實上,這也是所有的使用public繼承的動機:為基類類型實現的代碼對於派生類類型來說同樣適用。)
對於隨機訪問迭代器和雙向迭代器來說,advance的特化版本同時能夠做正向或者負向的移動,但是對於forward迭代器或者input迭代器來說,如果你想進行一個負向的移動就會出現未定義行為。實現中如果簡單的假設d是非負的,當傳遞一個負參數時,你就會進入一個很長的循環中,直到d變為0為止。在上面的代碼中,我所使用的替代方法是拋出一個異常。兩種實現都是有效的。這就是未定義行為的詛咒:你不能預測出來會發成什麼。
考慮為doAdvance所重載的不同版本,所有advance需要做的就是調用它們,傳遞一個額外的合適的迭代器類別對象,最後編譯器就能夠使用重載方案來調用合適的實現:
1 template<typename IterT, typename DistT> 2 void advance(IterT& iter, DistT d) 3 { 4 doAdvance( // call the version 5 iter, d, // of doAdvance 6 typename // that is 7 std::iterator_traits<IterT>::iterator_category() // appropriate for 8 ); // iter’s iterator 9 10 } // category
我們可以總結一下如何使用traits class:
Traits被廣泛使用在標准庫中。對於iterator_traits來說,除了iterator_category,還為迭代器提供了四種其它的信息(最有用的就是value_type—Item 42中給出了一個例子。)還有char_traits,存儲了字符類型的信息,numeric_limits,提供數字類型信息,例如,它們能夠表示的最大和最小值等等。(numeric_limits這個名字可能讓你感到意外,因為傳統的命名方式是以“traits”結尾,但是numeric_limits沒有遵守。)
TR1(Item 54)為了為類型提供信息引入了大量的新的traits class,包括is_fundamental<T>(判斷T是否為內建類型),is_array<T>(判斷T是否為數組),和is_base_of<T1,T2>(判斷T1和T2是否相同或者是T2的基類)。TR1向標准C++中添加了大概有50個traits classes。