1. 程式人生 > 其它 >SGI STL迭代器 iterators

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型別,而查詢目標元素是int型別。但ListIter中定義的operator!= 比較的是兩個相同的ListIter型別,因此沒有可用的operator!=,需要另外編寫一個全域性的operator!=過載函式,並用int和ListItem作為這2個引數型別:

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的模板引數T做了限制,即T必須是指標型別。

有了這個技法,可以解決前面“內嵌型別”沒能解決的問題。原來的問題是:原生指標並非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()會產生一個臨時物件(例如int()會產生一個臨時int物件一樣),其型別應該隸屬於前述5個迭代器之一。然後,根據這個型別,編譯器才決定呼叫哪個__advance()過載版本。

關於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替代了。

總結

  1. 設計適當的關聯型別(associated types),是迭代器的責任;設計適當的迭代器,是容器的責任。因為只有容器本身,才知道設計出怎樣的迭代器來遍歷自己,並執行迭代器的各種行為(前進,後退,取值,取用成員,...),至於演算法,完全可以獨立於容器和迭代器之外,只要設計以迭代器為對外介面即可。

  2. traits程式設計技法大量應用於STL實現中,利用“內嵌型別”的程式設計技巧和編譯器template引數推導功能,增強了C++未能提供的關於型別認證方面的能力。