CUJ:標準庫:定義iterator和const iterator (轉)

worldblog發表於2007-12-14
CUJ:標準庫:定義iterator和const iterator (轉)[@more@]

The Standard Librarian: Defining Iterators and Const Iterators

Matt Austern:namespace prefix = o ns = "urn:schemas--com::office" />

http://www.cuj.com/experts/1901/austern.htm?topic=experts

------------------------------------------------------------------------------------------------------------------------

  寫一個iterator並不難,並且它是擴充套件C++標準執行庫的一個自然方式。但如果你想做正確,還是一些應該知道的關鍵點的。

  標準C++執行庫被設計得可擴充套件的:標準演算法(如reverse()和partition())對預定義的容器(如vector和list)進行操作,並且它們也可以操作於自定義的提供了適當iterator的資料結構。對執行庫的高效使用包括對它的擴充。

  現如今,iterator對絕大多數的C++員都不陌生了。Iterator抽象了指標的絕大部分基本特徵:forward iterator指向序列中的某個,而且它能進行增量運算以指向序列中的下一個元素。(更強的iterator範疇,bidirectional iterator和ran iterator提供了額外的遍歷序列的方法。更弱的iterator範疇則對絕大多數的資料結構都是不合適的。)

  每iterator都有一個範疇型別category_type(對forward iterator來說則是std::forward_iterator_tag),一個value_type (它所指向的物件的型別),一個difference_type(表示序列中兩個元素間距離的一個整數型別),以及一個pointer_type和reference_type(指向iterator的value_type的指標和引用)。這些型別都可透過std::iterator_traits來訪問;當你定義自己的iterator類時,提供它們的最容易的方法是提供巢狀的typedefs: iterator_category,value_type,difference_type,pointer,和reference。

  所謂forward iterator就是任何滿足C++標準§24.1.3中的需求的型別;標準的那個小節告訴了你要定義什麼成員和過載哪些運算子。一旦你已經知道iterator需要掌握的資訊(以使得它能指向一個元素和找到其下一個元素),定義一個forward iterator只不過是對這些成員函式進行填空。 

配對的Iterator

使問題變複雜的一個因素是通常定義一個iterator類是不夠的。你可能需要定義兩個iterator類,一個允許修改它所指向的物件(*i返回物件的引用),而另一個不允許(*i返回一個const的引用)。執行庫預定義的容器類這麼做了:舉例來說,std::list類,有一個巢狀型別 iterator和另外一個不同的巢狀型別const_iterator;後者可以用來遍歷const std::list。list::iterator和list::const_iterator的value_type都是T,但reference_type和pointer_type不同:對list::iterator它們分別是T&和T*,而對list::const_iterator它們是const T&和const T*。你能將一個list::iterator轉換到一個list::const_iterator,但(由於const 的正確性的明顯理由)不能反過來。

  配對的iterator在使用者自定義型別中如同在標準執行庫預定義型別中一樣常見。舉例來說,假設你正在定義一個簡單的單向連結串列類。你可能這樣開始: 

template

struct slist_node {

  T val;

  slist_node* next;

  slist_node

  (const T& t, slist_node* p)

  : val(t), next(p) { }

};

template struct slist {

  slist_node* head;

    ...

};

  供slist用的iterator類同樣簡單:

template

struct slist_iterator {

  typedef std::forward_iterator_tag

  iterator_category;

  typedef T value_type;

  typedef std::ptrdiff_t

  difference_type;

  typedef T& reference;

  typedef T* pointer;

    slist_iterator(slist_node* x=0)

  : p(x) { }

  slist_iterator

  (const slist_iterator& i)

  : p(i.p) { }

  reference operator*() const

  { return p->val; }

  pointer operator->() const

  { return &(p->val); }

  slist_iterator& operator++() {

  p = p->next;

  return *this;

  }

  slist_iterator operator++(int) {

  slist_iterator tmp(*this);

  ++*this;

  return tmp;

  }

    slist_node* p;

};

  我們該如何定義相應的const iterator?我們可以定義一個獨立的 slist_const_iterator類,但程式碼重複造成浪費和易於出錯。將slist_iterator變成slist_const_iterator時,變化是極小的:

l  申明p的型別為const slist_node*而不是slist_node*。

l  申明pointer和reference為const T*和const T&。

l  定義一個轉換建構函式,它接受一個slist_iterator型別的引數。

  這並不妨礙定義單個類來同時取代slist_iterator和slist_const_iterator。我們將iterator定義為接受一些額外模板引數,這些引數決定它是否為一個const iterator。我們給這個類一個帶非const版本的引數的建構函式;一種情況下它將成為一個複製建構函式,而另一種情況下它將成為一個轉換建構函式。另外二個差異只涉及用一個型別代替另外一個,因此很容易將它們封裝入模板引數。

  最後:那些額外的模板引數應該看起來象什麼?在我的書中[注1],我建議pointer和reference型別作為模板引數顯式傳入。那個方法是可以的,但是它造成了多少有些笨重的型別名稱;有一個整潔的解決方案。我們可以只提供一個額外模板引數,一個布林標誌,以決定我們是否正在定義const iterator,然後使用一點小技巧:“編譯期的? : 操作”以根據此標誌選擇一個型別或另一個。這展示於Listing 1。

等於比較

我們還沒有定義一個相等操作。這中間還隱藏著一個問題,而你甚至能在一些標準執行庫預定義的iterator中發現它。試著編譯這一個程式:

#include

int main() {

  std::deque d;

  std::deque::const_iterator i = d.begin();

  while (d.end() != i)

  ++d;

}

  程式沒有做任何事,但問題不在於這一點。重要的是,對很多現存的執行庫的實作,它甚至不能編譯。那些實作有嗎?不一定;i的型別是deque::const_iterator,而d.begin()返回一個deque::iterator,C++標準沒有明確這兩者之間的等於比較是否保證能工作[注3]。然而,即使標準沒有明確要求這一點,如果你在你自己的iterator類中支援它的話,當然會更友好。

  你可能想知道這怎麼會成為問題。畢竟,我們不是已經說了容器的 iterator型別總能被轉換到它的const iterator型別嗎?如果d.begin()能轉換成deque<>::const_iterator,那麼為什麼不能比較他們?

  問題是有許多的不同方法來定義iterator的相等操作;如果它們是按兩種最顯而易見的方法來定義的,容器的iterator和const iterator型別之間的比較將不能工作。

  首先,假設operator==()被定義為成員函式。這不足夠好。如果i是 deque<>::const_iterator,而j是deque<>::iterator,那麼i == j可以工作而j == i不能。很容易明白不對稱的原因:成員函式天生就是不對稱的。a.f(b)這樣的(或,對本例,j.operator==(i))特定類的成員函式;轉換隻發生在函式的引數上。

  再明白不過了,於是,你的下個想法可能是定義operator==()為非成員函式。很不幸,這樣作更糟!一個簡單的程式舉例說明了問題: 

template struct A { };

template struct B {

  B() { }

  B(A) { }

};

 

template

void f(B, B) { }

  int main() {

  A a;

  B b(a); // OK, A is

  // convertible to B

  f(a, b);  // Doesn’t work

}

  A能轉換到B還不足夠。如果f不是模板,不會有問題:會應用使用者自定義的A到B的轉換。然而,因為f依賴於模板引數T,另一個步驟將先:編譯器必須推匯出一個T型別以使得函式的呼叫匹配於f的實參表。對本例,無法匹配:f的申明說它的實參應該是相同型別的,但我們試圖用兩個不同的型別來呼叫它。模板引數推導需要完全匹配[注4];使用者自定義的轉換操作不會被考慮。

  我們不能申明operator==()為成員函式,也不能是非成員模板函式。看起來我們需要申明一系列的非模板函式,對應於每個可能的iterator類例項。這是一個奇怪的需求,因為一系列引數化的函式正是模板的功能,但最奇怪的是它實際上是可能的。

  它之所以可能,是因為類别範本中友元申明上的一個含糊的[注5](WQ注,參看Herb Sutter的文章《Sutter's Mill: Befriending Templates》,CSDN上亦有我譯的中文版)。你能顯式申明友元函式是一個模板(的例項)。然而,如果你沒有,並且如果它不匹配於已經被申明的函式模板,那麼編譯器將假設它是一個普通的非模板函式。舉例來說:

template void g(T);

 

template struct X {

  // f is a function template

  friend void f(T);

    // g is a function template

  friend void ::g(T); 

    // h is a non-template function

  friend void h(T); 

};

  通常這隻會造成麻煩:正常情況下你期望編譯期將h這樣的東西認為是函式模板的申明,於是你不得不記住將它按編譯器認為的方式申明。然而,有時候,這個怪異之事還真的有用。如果在友元宣告時同時定義這個函式,並且申明它為一個非模板友元,你將會得到一系列的非模板函式--正是我們在iterator的相等操作上所需要的。 

template struct X {

  friend void f(T) { } // f is a non-template function

};

  slist_iterator的完全的定義,包括相等比較,見於Listing 2。

總結

  當你寫一個容器或象容器的東西時,通常定義一個iterator類是不夠的。你需要定義配對的iterator和const iterator類。定義這樣的配對類會帶來一些實現上的問題,這些問題是在只定義單個iterator類時是沒有的。

  iterator/const iterator這兩個類必須具有相同的iterator category,defference type和value_type;iterator類必須能轉換到const iterator類,但不能反過來。你將iterator和const iterator定義為一個類,透過增加額外的模板引數並使用choose模板(或類似的東西)來定義適當的巢狀型別。如果你正在使用預定義的容器類(string,vector,list,deque,set,map,mulitset,multimap),應該避免對它的一個iterator和一個const iterator進行比較。如果d是deque(反過來就是const deque&),你不應該寫:

std::deque::const_iterator i = d.begin();

while (i != d.end())

  ...

  你應該寫:

std::deque::iterator i = d.begin();

while (i != d.end())

  ...

  C++標準沒有明確說第一種形式應該能工作,並且,的確,它不能在所有實作上都工作。如果避免它,你的程式將更可移植。當你正在定義你自己的配對的iterator和const iterator類時,你能容易地確保兩者間的比較將正確工作;只要確保定義比較操作為非模板的友元函式。

Listing 1: An Slist_iterator class, complete except for the equality operator

template

struct choose;

 

template

struct choose {

  typedef IsTrue type;

};

 

template

struct choose {

  typedef IsFalse type;

};

 

template

struct slist_iterator {

  typedef std::forward_iterator_tag iterator_category;

  typedef T value_type;

  typedef std::ptrdiff_t difference_type;

  typedef typename choose::type

  reference;

  typedef typename choose::type

  pointer;

 

  typedef typename choose*, 

  slist_node*>::type

  nodeptr;

 

  slist_iterator(nodeptr x = 0) : p(x) { }

  slist_iterator(const slist_iterator& i)

  : p(i.p) { }

  reference operator*() const { return p->val; }

  pointer operator->() const { return &(p->val); }

  slist_iterator& operator++() {

  p = p->next;

  return *this;

  }

  slist_iterator operator++(int) {

  slist_iterator tmp(*this);

  ++*this;

  return tmp;

  }

 

  nodeptr p;

};

 

— End of Listing —

Listing 2: A complete implementation of Slist_iterator and a partial implementation of an Slist container

template struct slist_node {

  T val;

  slist_node* next;

  slist_node(const T& t, slist_node* p)

  : val(t), next(p) { }

};

 

template

struct choose;

 

template

struct choose {

  typedef IsTrue type;

};

 

template

struct choose {

  typedef IsFalse type;

};

 

template

struct slist_iterator {

  typedef std::forward_iterator_tag iterator_category;

  typedef T value_type;

  typedef std::ptrdiff_t difference_type;

  typedef typename choose::type

  reference;

  typedef typename choose::type

  pointer;

 

  typedef typename choose*, 

  slist_node*>::type

  nodeptr;

 

  slist_iterator(nodeptr x = 0) : p(x) { }

  slist_iterator(const slist_iterator& i)

  : p(i.p) { }

  reference operator*() const { return p->val; }

  pointer operator->() const { return &(p->val); }

  slist_iterator& operator++() {

  p = p->next;

  return *this;

  }

  slist_iterator operator++(int) {

  slist_iterator tmp(*this);

  ++*this;

  return tmp;

  }

 

  friend bool operator==(const slist_iterator& x,

  const slist_iterator& y) {

  return x.p == y.p;

  }

 

  friend bool operator!=(const slist_iterator& x,

  const slist_iterator& y) {

  return x.p != y.p;

  }

 

  nodeptr p;

};

 

// This is not a complete container class.  It is just

// enough to illustrate defining and using an iterator/

// const iterator pair.

template struct slist {

  slist_node* head;

 

  typedef slist_iterator  iterator;

  typedef slist_iterator const_iterator;

 

  iterator begin() { return iterator(head); }

  iterator end()  { return iterator(0); }

  const_iterator begin() const {

  return const_iterator(head);

  }

  const_iterator end() const {

  return const_iterator(0);

  }

 

  slist() : head(0) { }

  ~slist() {

  while (head) {

  slist_node* next = head->next;

  delete head;

  head = next;

  }

  }

 

  void push_front(const T& t) {

  head = new slist_node(t, head);

  }

 

  ...

};

— End of Listing —

 

Notes and References

[1] Matt Austern. Generic Programming and the STL (Addison-Wesley, 1998).

[2] Using partial specialization to define a compile-time ?: operator (as well as other compile-time Boolean operations) is a relatively old a; using it as a clean solution to the iterator/const iterator problem is more recent. I learned of the latter idea from Jeremy Siek.

[3] This is an open issue under consideration by the C++ standardization committee. See .

[4] The full rules for template argument deduction are described in §14.8.2.1 of the C++ Standard.

[5] See §14.5.3 of the C++ Standard.

 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-993496/,如需轉載,請註明出處,否則將追究法律責任。

相關文章