SGI STL迭代器 iterators
迭代器的設計思想
GoF提到iterator設計模式:
提供一種方法,使之能依序巡訪某個聚合物(容器)所含的各個元素,而又無須暴露該聚合物的內部表述方式。
STL中的iterator(迭代器)正是踐行了這些設計模式,其中心思想:將資料容器(containers)和演算法(algorithm)分離開,彼此獨立設計,最後用膠著劑將它們粘合到一起。如何將容器和演算法良好地膠合到一起,是重難點所在。
如何才算是將容器和演算法良好粘合到一起呢?
以std::find()演算法為例,其原始碼:
// from SGI <stl_algo.h> template<class InputIterator, class T> InputIterator find(InputIterator first, InputIterator last, const T& value) { while (first != last && *first != value) ++first; return first; }
可見,find演算法並不知道具體的迭代器型別,也不知道其內部細節,只是對迭代器呼叫了operator!=、operator++、解引用(dereference)操作,對迭代器所指元素呼叫operator!=。
也就是說,如果我們傳入find()的迭代器實參,實現了這幾個介面即可。
這樣,對於不同迭代器型別,find()也能很好地工作:
#include <vector> #include <list> #include <deque> #include <algorithm> #include <iostream> using namespace std; int main() { const int arraySize = 7; int ia[arraySize] = {0,1,2,3,4,5,6}; vector<int> ivect(ia, ia + arraySize); list<int> ilist(ia, ia + arraySize); deque<int> ideque(ia, ia + arraySize); vector<int>::iterator it1 = find(ivect.begin(), ivect.end(), 4); /* ivect中查詢元素4 */ if (it1 == ivect.end()) cout << "4 not found." << endl; else cout << "4 found. " << *it1 << endl; // 執行結果: 4 found. 4 list<int>::iterator it2 = find(ilist.begin(), ilist.end(), 6); if (it2 == ilist.end()) cout << "6 not found." << endl; else cout << "6 found. " << *it2 << endl; // 執行結果: 6 found. 6 deque<int>::iterator it3 = find(ideque.begin(), ideque.end(), 8); if (it3 == ideque.end()) cout << "8 not found." << endl; else cout << "8 found. " << *it3 << endl; // 執行結果: 8 not found. return 0; }
整個過程,find演算法不知道迭代器內部細節,也不知道容器的細節,容器也不知道演算法的細節。迭代器就是粘合容器和find演算法的膠合劑。
迭代器的本質:smart pointer
迭代器行為類似於指標,但並不等於指標,而是類似於指標的物件,因為迭代器在指標的基礎上,還包裹了其他功能,比如過載運算子operator,operator->。
指標的核心行為是解引用(dereference)、成員訪問(member access),對應地,迭代器重要程式設計工作就是過載(overloading) 運算子operator、operator->。
假設我們現在有一個list(STL中list是雙向連結串列,這裡為了方便只設計成單向連結串列),要為其設計一個迭代器。
list容器:
template<typename T>
class List
{
void insert_front(T value);
void insert_end(T value);
void display(std::ostream& os = std::cout) const;
// ...
private:
ListItem<T>* _end;
ListItem<T>* _front;
long _size;
};
template<typename T>
class ListItem
{
public:
T value() const { return _value; }
ListItem* next() const { return _next; }
...
private:
T _value;
ListItem* _next; /* 單向連結串列 */
};
如何讓這個List能使用前面的std::find()演算法?
一種簡單做法,就是按std::find()用到的迭代器的運算子,為我們要設計的迭代器ListIter過載operator*, operator->等運算子。
// List的迭代器類模板
template<class Item> /* Item可以是單向連結串列節點或雙向連結串列節點, 該迭代器只為連結串列服務 */
struct ListIter
{
Item* ptr; // 保持與容器之間的一個聯絡
ListIter(Item* p = 0) // default ctor
: ptr(p) {}
// 不必實現copy ctor, default版本即可
// 不必實現operator=, default版本即可
Item& operator*() const { return *ptr; }
Item* operator->() const { return ptr; }
// 以下2個operator++遵循標準做法
// 1. pre-increment operator 前置式遞增
ListIter& operator++()
{ ptr = ptr->next(); return *this; }
// 2. post-increment operator 後置式遞增
ListIter operator++(int)
{ ListIter tmp = *this; ++*this; return tmp; }
bool operator==(const ListIter& other) const
{ return ptr == other.ptr; }
bool operator!=(const ListIter& other) const
{ return ptr != other.ptr; }
};
現在,可以將List和find()通過ListIter粘合起來了:
void main()
{
List<int> mlist;
for (int i = 0; i < 5; ++i) {
mlist.insert_front(i);
mlist.insert_end(i+2);
}
mlist.display(); // 10 {4 3 2 1 0 2 3 4 5 6}
ListIter<ListItem<int>> begin(mlist.front());
ListIter<ListIterm<int>> end; // default 0, null
ListIter<ListIterm<int>> iter; // default 0, null
iter = std::find(begin, end, 3);
if (iter == end)
cout << "not found" <<endl;
else
cout << "found. " << iter->value() << endl;
// 執行結果 found. 3
iter = std::find(begin, end, 7);
if (iter == end)
cout << "not found" << endl;
else
cout << "found. " << iter->value() << endl;
//執行結果 not found
}
由於find()內部呼叫了*iter != value 來檢查迭代器所指元素跟目標元素是否相等,而iter所指元素是ListItem
template<typename T>
bool operator!=(const ListItem<T>& item, T n)
{
return item.value() != n;
}
可看到這種設計並不好,因為暴露了太多List實現細節,客戶端main()為了得到begin、end迭代器,不得不知道ListItem存在;ListIter class template中,為了實現operator++,還暴露了ListItem的next()函式;而且還不得不實現一個全域性的operator!=。
下面,我們看如何用traits程式設計技法來解決這個問題。
迭代器關聯型別 associated types
在演算法中使用迭代器時,可能會用到關聯型別(associated type),即迭代器所指之物的型別。
如果演算法有必要宣告一個變數,以“迭代器所指物件的型別”為型別,如何是好?
因為C++只支援sizeof(),不支援typeof()(GCC編譯器操作符),而且RTTI性質(執行時型別識別)中的typeid(),獲得的也只是型別名稱(字串),不能拿來做變數宣告、定義。
一個比較好的解決辦法是,利用function template的引數推導(argument deducation)機制。例如,
template<class I, class T>
void func_impl(I iter, T t)
{
T tmp; // 這裡解決了問題, T就是迭代器所指之物的型別, 本例中為int
// ... 做原本func()應該做的全部工作
}
template<class I>
inline void func(I iter) // 這裡只給func傳入一個實參iter, 由function template機制推匯出iter型別I
{
func_impl(iter, *iter); // func的工作全部移往func_impl
}
int main()
{
int i;
func(&i); // 例:如何在演算法中定義一個由&i推匯出的變數型別?
}
例子中,func()作為對外介面,把實際操作全部置於func_impl()中。func()利用function template機制,根據傳入實參推匯出iter型別I。然後在func_impl定義iter關聯型別的變數tmp。
traits程式設計技法
迭代器所指物件的型別,稱為該迭代器的value type。
注意與關聯型別區別:關聯型別是一種概念,value type是具體的型別(是一種特性),value type可以用來表述關聯型別的具體型別。
template的引數推導機制雖好,可用於推匯出value type,但僅適用於函式引數型別,而不適用於函式返回值型別。
因此,可以把template的引數型別推導,宣告為內嵌型:
// 為迭代器MyIter內嵌型別value_type
template<class T>
struct MyIter // 陷阱:如果迭代器是原生指標,根本就沒這樣一個class type
{
typedef T value_type; // 內嵌型別宣告(nested type)
T* ptr;
MyIter(T* p = 0) : ptr(p) { }
T& operator*() const { return *ptr; }
// ...
};
// 將“template的引數型別推導”機制,針對value type,專門寫成一個function template
template<class I>
typename I::value_type func(I ite) // typename I::value_type是func的返回值型別
{ return *ite; }
// 客戶端
// ...
MyIter<int> ite(new int(8)); // 定義迭代器ite, 指向int 8
cout << func(ite); // 輸出:8
// 如果傳給func的引數(迭代器),是一個原生指標,這種方式就失效了
注意:func()返回值型別必須加上關鍵字typename,因為T是一個template引數,在編譯器具現化前,編譯器不知道T是什麼。也就是說,編譯器不知道typename I::value_type 是一個型別,member function,還是data member。用關鍵字typename顯式告訴編譯器,這是一個型別,從而通過編譯。(見Effective C++ 條款42)
宣告內嵌型別有一個陷阱:不是所有迭代器都是class type,比如原生指標(native pointer)就不是。如果不是class type,就無法為它定義內嵌型別,但STL又必須接受原生指標作為迭代器,那要怎麼辦?
答案是可以針對這種特定情況,使用template partial specialization(模板偏特化)做特殊化處理。
Partial Specialization 偏特化
我們知道,一個class template引數包含模板引數型別、個數這2部分資訊,偏特化是在這兩方面做特殊化處理,但不是針對具體的特定型別作處理,而是作為通用模板的子集。針對具體型別做特殊處理,稱為模板特例化(簡稱特化,template specialization),而不是偏特化(template partial specialization)。
關於模板特化、偏特化,可以參見C++ Primer學習筆記 - 模板特例化
假設有個class template:
// 通用版 class template
template<typename T>
class C{...} // 這個泛化版本允許(接受)T為任何型別
對T型別做出限制,讓其僅適用於“T為原生指標”的情況,可以知道是一個partial specialization:
template<typename T>
class C<T*>{...} // 偏特化版class template,僅適用於“T為原生指標”的情況
// “T為原生指標”是“T為任何型別”的一個更進一步的條件限制
注:偏特化的class C<T*>仍然是一個模板,不過針對class C
有了這個技法,可以解決前面“內嵌型別”沒能解決的問題。原來的問題是:原生指標並非class,無法定義內嵌型別(value_type)。現在,可以利用模板偏特化針對“迭代器的template引數為指標”的情況,設計特化版的迭代器。
如何針對迭代器引數為指標的情況,設計特化版迭代器,且看下面的 萃取內嵌型別。
萃取內嵌型別
參考前面用於提取迭代器的value_type特性的function template func(),寫出一個更通用的class template,專門用來“萃取”迭代器的value type特性:
// 萃取內嵌型別value type
template<class I>
struct iterator_traits { // traits意為“特性”,用於萃取出模板引數I的關聯的原生型別value type
typedef typename I::value_type value_type;
};
所謂traits,意義是,如果模板引數I定義有自己的value type,那麼通過traits的作用,萃取出來的value_type就是I::value_type。
這樣,如果I定義自己的value type,前面func具體可以改寫:
template<class T>
tyename I::value_type func(I ite) // typename I::value_type是func返回值型別
{ return *ite; }
// 改寫 ==>
template<class T>
typename iterator_traits<I>::value_type // 這一整行是函式返回值型別
func(I ite)
{ return *ite; }
多了一層間接性(利用iterator_traits<>來做萃取),好處是traits可以擁有特化版本。現在,可以讓iterator_traits針對原生指標,擁有一個partial specialization:
// 原生指標不是class,沒有內嵌型別,通過偏特化版本定義value type
template<class T>
struct iterator_traits<T*> // 針對原生指標設計的偏特化版iterator_traits
{
typedef T value_type;
};
這樣,雖然原生指標int*不是一種class type,但也可以通過traits技法萃取出value type(iterator_traits偏特化版為其定義的,關聯型別)。如此,就解決了先前的問題。
去掉常量性
針對“指向常數物件的指標(pointer-to-const)”,下面式子得到什麼結果?
iterator_traits<const int*>::value_type
得到的是const int,而非int。然而,這並不是我們期望的,因為我們希望利用這種機制宣告一個暫時變數,使其型別與迭代器的value type相同。而現在,宣告一個無法賦值的臨時變數(因為const屬性),對我們沒什麼用。因此,如果迭代器是個pointer-to-const,我們應該設法令其value type為一個non-const型別。對此,設計一個特化版本:
// 通過針對pointer-to-const設計的偏特化版本,為萃取關聯型別去掉const
template<class T>
struct iterator_traits<const T*>
{
typedef T value_type; // 注意這裡value_type不再是const型別, 通過該偏特化版已經去掉了const屬性
};
這樣,不論面對自定義迭代器MyIter,原生指標int,還是pointer-to-const的const int,都可以通過iterator_traits萃取出正確的value type。
迭代器特性
traits扮演“特性萃取機”的角色,針對迭代器的traits稱為iterator_traits,用於萃取各個迭代器特性。而迭代器特性,是指迭代器的關聯型別(associated types)。為了讓traits能正常工作,每個迭代器必須遵守約定,以內嵌型別定義(nested typedef)的方式,定義出關聯的型別。這個約定,誰不遵守,後不能相容STL。
當然,迭代器常用的關聯型別不止value type,還有另外4種:difference type,pointer,reference,iterator category。如果希望開發的容器能與STL融合,就必須為你的容器的迭代器定義這5種類型。
如此,“特性萃取機”traits就能忠實地將各種特性萃取出來:
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;
};
另外,iterator_traits必須針對傳入型別為pointer、pointer-to-const者,設計偏特化版。
迭代器關聯型別 value type
所謂value type,是指迭代器所指向的物件的型別,任何一個打算與STL演算法完美搭配的class,都應該定義自己的value type內嵌型別。
迭代器關聯型別 difference type
difference type 用來表示兩個迭代器之間的距離,因此也可以用來表示容器的最大容量,因為對於連續空間的容器而言,頭尾之間的距離就是最大容量。如果一個泛型演算法提供計數功能,如STL count(),其返回值必須使用迭代器的difference type。
例如,std::count()演算法對迭代器區間對值為value的元素進行計數:
template<class I, class T>
typename iteartor_traits<I>::difference_type // 這一整行是函式返回型別
count(I first, I last, const T& value)
{
typename iteartor_traits<I>::difference_type n = 0; // 迭代器之間的距離
for (; first != last; ++first)
++n;
return n;
}
原生指標的difference type
同value type,iterator_traits無法為原生指標內嵌difference type,需要設計特化版本。具體地,以C++內建ptrdiff_t(<stddef.h>)作為原生指標的difference type。
// 通用版,從型別I萃取出difference type
template<class I>
struct iterator_traits
{
...
typedef typename I::difference_type difference_type;
};
// 針對原生指標而設計的偏特化版
template<class I>
struct iterator_traits<T*>
{
...
typedef ptrdiff_t difference_type;
};
// 針對原生pointer-to-const而設計的偏特化版
template<class I>
struct iterator_traits<const T*>
{
...
typedef ptrdiff_t difference_type;
};
這樣,任何時候,我們需要任何迭代器I的difference type的時候,可以這樣寫,而不論I是class type,還是pointer,或者const-to-pointer:
typename iterator_traits<I>::difference_type
迭代器關聯型別 reference type
從“迭代器所指之物的內容是否允許改變”的角度看,迭代器分為兩種:
1)不允許改變“所指物件的內容”,稱為constant iterators(常量迭代器),例如const int* pic;
2)允許改變“所指物件的內容”,稱為mutable iterators(擺動迭代器),例如int* pi;
當對一個mutable iterators進行解引用(dereference)時,獲得不應該是一個右值(rvalue),而應當是一個左值(lvalue)。因為右值允許賦值操作(assignment),左值才允許。
而對一個constant iterator進行解引用操作時,獲得的是一個右值。
int* pi = new int(5);
const int* pci = new int(9);
*pi = 7; // mutable iterator進行解引用操作時, 獲得的是左值, 允許賦值
*pci = 1; // 不允許賦值, 因為pci是const iterator, 解引用pci獲得的是右值
C++中,函式如果要傳回左值,都是以by reference方式進行,所以當p是個mutable iterators時,如果是其value type是T,那麼p的型別不應該是T,而應該是T&。
如果p是個constant iterators,其value type是T,那麼p的型別不應該是const T,而應該是const T&。
這裡討論的*p的型別,也就是reference type。其實現細節,在下一節跟pointer type一起描述。
迭代器關聯型別 pointer type
pointer與reference在C++關係非常密切。如果“傳回一個左值,令它代p表所指之物”(reference)是可能的,那麼“傳回一個左值,令它代表p所指之物的地址”(pointer)也一定可以。pointer,就是指向迭代器所指之物。
reference type, pointer type型別,之前在ListIter class template中已經出現過:
Item& operator*() const { return *ptr; } // 返回值型別是reference type
Iterm* operator->() const { return ptr; } // 返回值型別是pointer type
現在,把reference type和pointer type這兩個型別加入traits:
// 通用版traits
template<class I>
struct iterator_traits
{
...
typedef typename I::pointer pointer; // native pointer 無法內嵌pointer, 因此需要偏特化版
typedef typename I::reference reference; // native pointer 無法內嵌reference
};
// 針對原生指標的偏特化版traits
template<class I>
struct iterator_traits<T*>
{
...
typedef T* pointer;
typedef T& reference;
};
// 針對pointer-to-const的偏特化版traits,去掉了const常量性
template<class I>
struct iterator_traits<const T*>
{
...
typedef T* pointer;
typedef T& reference;
};
迭代器關聯型別 iterator_category
先討論迭代器分類。
迭代器型別
根據移動特性和施行操作,迭代器被分為5類:
- Input Iterator:這種迭代器所指物件,不允許外界改變,只讀(read only)。
- Output Iterator:只寫(write only)。
- Forward Iterator:允許“寫入型”演算法(如replace()),在此種迭代器所形成的的區間上進行讀寫操作。
- Bindirectional Iterator:可雙向移動。某些演算法需要逆向走訪某個迭代器區間(錄入逆向拷貝某範圍內的元素),可使用Bindirectional Iterator。
- Random Access Iterator:前4種迭代器都只供應一部分指標算術能力(前3支援operator++,第4中加上operator--),第5中則涵蓋所有指標算術能力,包括p+n, p-n,p[n],p1-p2,p1 < p2。
這些迭代器分類與從屬關係:
注:直線與箭頭併發C++繼承關係,而是所謂concept(概念)與refinement(強化)關係。
設計演算法時,如果可能,應儘量針對某種迭代器提供一個明確定義,並針對更強化的某種迭代器提供另一種定義,重複利用迭代器特性。這樣,才能在不同情況下,提供最大效率。
以advance()為例
advance()是一個許多演算法內部常用的函式,功能是內部將p累進n次(前進距離n)。該函式2個引數:迭代器p,數值n。下面3分定義:分別針對Input Iterator,Bidirectional Iterator,Random Access Iterator。沒有針對Forward Iterator設計的版本,因為和針對Input Iterator設計的版本完全一致。
// 針對InputIterator的advance()版本
// 要求 n > 0
template<class InputIterator, class Distance>
void advance_II(InputIterator& i, Distance n)
{
// 單向,逐一前進
while (n--) ++i; // or for(; n > 0; --n; ++i);
}
// 針對BidirectionalIterator的advance()版本
// n沒有要求,可大於等於0,也可以小於0
template<class BidirectionalIterator, class Distance>
void advance_BI(BidirectionalIterator& i, Distance n)
{
// 雙向,逐一前進
if (n >= 0)
while (n--) ++i; // or for (; n > 0; --n, ++i);
else
while (n++) --i; // or for (; n < 0; ++n, --i);
}
// 針對RandomAccessIterator的advance()版本
// n沒有要求
template<class RandomAccessIterator, class Distance>
void advance_RAI(RandomAccessIterator& i, Distance n)
{
// 雙向,跳躍前進
i += n;
}
現在,當程式呼叫advance()時,應選擇哪個版本的函式呢?
如果選擇advance_II(), 對Random Access Iterator效率極低,原本O(1)操作成了O(N);如果選擇advance_RAI(),則無法接收Input Iterator(Input Iterator不支援跳躍前進)。
我們可以將三者合一,對外提供統一介面。其設計思想是根據迭代器i的型別,來選擇最適當的advance()版本:
tempate<class InputIterator, class Distance>
void advance(InputIterator& i, Distance n)
{
if (is_random_access_iterator(i)) // 函式有待設計
advance_RAI(i, n);
else if (is_bidirectional_iterator(i)) // 函式有待設計
advance_BI(i, n);
else
advance_RAI(i, n);
}
這種方法的問題在於:在執行期才決定使用哪個版本,而且需要一個個判斷,會影響程式效率。最好能在編譯期就能選擇正確的版本。函式的過載機制能達成這個目標。
上面的3個advance_xxx()都有2個函式引數(i,n),型別都未定(因為是template引數)。為了讓這3個函式同名,形參過載函式,可以加上一個型別已經確定的函式引數,使得函式過載機制能有效運作起來。
設計考慮:如果traits有能力萃取出迭代器的型別,便可以利用這個“迭代器型別”的關聯型別,作為advance()的第三個引數,讓編譯器根據函式過載機制自動選擇對應版本advance()。這個關聯型別一定是一種class type,不能是數值、號碼類的東西,因為編譯器需要依賴它進行函式過載決議(overloaded resolution),而函式過載是根據引數型別來決定的。
下面定義5個class,作為迭代器的型別,代表5種迭代器型別:
// 5個作為標記用的型別(tag types)
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_iterator_tag : public bidirectional_iterator_tag {};
這5個class只用作標記,不需要任何成員,因此不會有任何記憶體代價。現在重新設計__advance()(由於只在內部使用,因此所有函式名前加上特定前導符),並加上第三個引數,使之形成過載:
// 如果迭代器型別是input_iterator_tag,就會dispatch至此
template<class InputIterator, class Distance>
inline void __advance(InputIterator& i, Distance n, input_iterator_tag)
{
// 單向, 逐一前進
while (n--) ++i;
}
// 如果迭代器型別是forward_iterator_tag,就會dispatch至此
// 這是一個單純的傳遞呼叫函式(trivial forwarding function), 稍後討論如何免除之
template <class ForwardIterator, class Distance>
inline void __advance(ForwardIterator& i, Distance n, forward_iterator_tag)
{
// 單純地進行傳遞呼叫(forwarding)
advance(i, n, input_iterator_tag());
}
// 如果迭代器型別是bidirectional_iterator_tag,就會dispatch至此
template<class BidirectionalIterator, class Distance>
inline void __advance(BidirectionalIterator& i, Distance n, bidirectional_iterator_tag)
{
// 雙向, 逐一前進
if (n >= 0)
while (n--) ++i;
else
while (n++) --i;
}
template<class RandomAccessIterator, class Distance>
inline void __advance(RandomAccessIterator& i, Distance n, random_access_iterator_tag)
{
// 雙向, 跳躍前進
i += n;
}
注意:每個__advanec()最後一個引數都只宣告型別,而未指定引數名稱,因為純粹只是用來啟用過載機制,函式中不使用該引數。如果要加上引數名也可以,但不會用到。
至此,還需要對外提供一個上層控制介面,呼叫上述各個過載的__advance()。介面只需要兩個引數,當它準備將工作轉給上述的__advance()時,才自行加上第三引數:迭代器型別。因此,這個上層介面函式必須有能力從它所獲得的迭代器中推匯出其型別,這個工作可以交給traits機制:
// 介面函式, 利用iterator_traits萃取迭代器的型別特性iterator_category
template<class InputIterator, class Distance>
inline void advance(InputIterator& i, Distance n)
{
__advance(i, n, iterator_traits<InputIteartor>::iterator_category());
}
iterator_traits
關於iterator_category(),SGI STL 定義於<stl_iterator.h>。原始碼如下:
// iterator_category() 返回一個臨時物件,型別是引數I的迭代器型別(iterator_category)
template<class I>
inline typename iterator_traits<I>::iterator_category // 一整行是含返回型別
iterator_category(const I&)
{
typedef typename iterator_traits<I>::iterator_category category;
return category(); // 返回臨時物件
}
相應地,也應該在traits新增一個可萃取的型別特性(iterator_category),並針對native pointer和pointer-to-const設計偏特化版本:
// 通用版traits, 可用於萃取I的iterator_category
template<class I>
struct iteartor_traits
{
...
typedef typename I::iterator_category iterator_category; // 為traits新增萃取特性iterator_category
};
// 針對原生指標設計的 偏特化版本
template<class T>
struct iterator_traits<T*>
{
...
//注意, 原生指標是一種Random Access Iterator. why?
typedef random_access_iterator_tag iterator_category;
};
// 針對原生的pointer-to-const設計的偏特化版本
template<class T>
struct iterator_traits<const T*>
{
...
// 注意, 原生的pointer-to-const是一種Random Access Iterator
typedef random_access_iterator_tag iterator_category;
};
問題:註釋裡面提到“原生指標是一種Random Access Iterator. ?”為什麼?
任何迭代器,其型別永遠應該落在“該迭代器所隸屬之各種型別中,最強化的那個”。例如,int*,既是Random Access Iterator,又是Bindirectional Iterator,同時也是Forward Iterator,而且是Input Iterator,那麼其型別應該是最強化的random_access_iterator_tag。
問題:為什麼advance()的template引數名稱,是最低階的InputIterator?而不是最強化的那個?
advance()能接受各種型別的迭代器,但其型別引數命名為InputIterator,這是STL演算法的一個命名規則:以演算法鎖能接受的最低階迭代器型別,來為其迭代器型別引數命名。
-
消除“單純傳遞呼叫的函式”
用class來定義迭代器的各種分類標籤,不僅可以促成過載機制運作,是的編譯器能正確執行過載決議,還可以通過繼承,我們不必再寫“單純只做傳遞呼叫的函式”,如前面__advance()的ForwardIterator版。
為什麼?
因為編譯器會優先匹配實參與形參完全匹配的函式版本,然後是從繼承關係來匹配。這也是為什麼5個迭代器型別中,存在繼承關係。
...
// 前面提到的這個單純的傳遞呼叫函式的__advance()版本,無需定義,因為forward_iterator_tag繼承自input_iterator_tag,編譯器沒有匹配到與forward_iterator_tag嚴格匹配的版本時,就會從繼承關係來匹配input_iterator_tag版的__advance()
// 如果迭代器型別是forward_iterator_tag,就會dispatch至此
// 這是一個單純的傳遞呼叫函式(trivial forwarding function)
template <class ForwardIterator, class Distance>
inline void __advance(ForwardIterator& i, Distance n, forward_iterator_tag)
{
// 單純地進行傳遞呼叫(forwarding)
advance(i, n, input_iterator_tag());
}
...
關於編譯器在函式過載決議時,如何選擇與實參型別匹配的版本。且看下面這個例子:
#include <iostream>
#include <string>
using namespace std;
struct B {};
struct D1 : public B {};
struct D2 : public D1 {};
template<class I>
void func(I& p, B)
{
cout << "B version" << endl;
}
template<class I>
void func(I& p, D2)
{
cout << "D2 version" << endl;
}
int main()
{
int* p;
func(p, B()); // 引數完全吻合, 輸出"B version"
func(p, D1()); // 引數未能完全吻合, 因繼承關係自動傳遞呼叫輸出"B version"
func(p, D2()); // 引數完全吻合, 輸出"D2 version"
return 0;
}
其中,func(p, D1())沒有找到型別嚴格相同的函式版本時,會從繼承關係中找。
以distance()為例
distance() 是一個常用迭代器操作函式,用來計算兩個迭代器之間的距離。針對不同迭代器型別,有不同的計算方式,帶來不同的效率。
這裡不再詳述推匯出distance的過程,而是直接貼出原始碼:
// 2個__distance()過載函式
// 如果迭代器型別是input_iterator_tag,就dispatch至此
template<class InputIterator>
inline typename iterator_traits<InputIterator>::difference_type
__distance(InputIterator first, InputIterator last,
input_iterator_tag) {
typename iterator_traits<InputIterator>::difference_type n = 0;
// 逐一累計距離
while (first != last) {
++first; ++n;
}
return n;
}
// 如果迭代器型別是random_access_iterator_tag,就dispatch至此
template<class RandomAccessIterator>
inline typename iterator_traits<RandomAccessIterator>::difference_type
__distance(RandomAccessIterator first, RandomAccessIterator last,
random_access_iterator_tag)
{
// 直接計算差距
return last - first;
}
// 對使用者介面, 可以自動推匯出型別, 然後編譯器根據推匯出的iterator_category特性,
// 自動選擇呼叫__distance()過載函式版本
/* 上層函式, 從所得的迭代器中推匯出型別 */
template<class InputIterator>
inline typename iterator_traits<InputIterator>::difference_type
distance(InputIterator first, InputIterator last)
{
typedef typename iterator_traits<InputIterator>::iterator_category category; // 萃取迭代器型別iterator_category
return __distance(first, last, category()); // 根據迭代器型別,選擇不同的__distance()過載版本
}
std::iterator的約定
前面講過,為了符合規範,任何迭代器必須遵守一定的約定,提供5個內嵌關聯型別,以便於traits萃取;否則,無法相容STL。但如果每寫一個迭代器,都要提供這5個內嵌關聯型別,誰能保證每次都不漏掉或者出錯?有沒有一種更簡便方式?
答案是有的,SGI STL在<stl_iterator.h>中提供一個公共iterator class,每個新設計的迭代器只需要繼承它,就可以保證符合STL所需要規範:
template <class _Category, class _Tp, class _Distance = ptrdiff_t,
class _Pointer = _Tp*, class _Reference = _Tp&>
struct iterator {
typedef _Category iterator_category;
typedef _Tp value_type;
typedef _Distance difference_type;
typedef _Pointer pointer;
typedef _Reference reference;
};
iterator class不含任何成員,純粹只是型別定義。因此繼承自它並不會有任何額外負擔(無執行負擔、無記憶體負擔)。而且後三個模板引數由於有預設值,新迭代器可以無需提供實參。
例如,前面自定義ListIter,改用繼承自iterator方式,可以這些編寫:
template<class Item>
struct ListIter : public iteratr<forward_iterator_tag, Item>
{
// ...
};
這樣的好處是很明顯的,可以極大地簡化自定義迭代器類ListIter的設計,使其專注於自己的事情,而且不容易出錯。
當然,我們也可以從SGI STL原始碼中看到,為5個迭代器型別input_iterator、output_iterator、forward_iterator、bidirectional_iterator、random_access_iterator都提供了各自的定義。這是為了向後相容HP STL,實際上目前已經被struct iterator替代了。
總結
-
設計適當的關聯型別(associated types),是迭代器的責任;設計適當的迭代器,是容器的責任。因為只有容器本身,才知道設計出怎樣的迭代器來遍歷自己,並執行迭代器的各種行為(前進,後退,取值,取用成員,...),至於演算法,完全可以獨立於容器和迭代器之外,只要設計以迭代器為對外介面即可。
-
traits程式設計技法大量應用於STL實現中,利用“內嵌型別”的程式設計技巧和編譯器template引數推導功能,增強了C++未能提供的關於型別認證方面的能力。