Iteratorについて
前の例会の講座でIteratorについての説明を端折ったのでここで説明しておく。
Iteratorとは、その名の通り、反復(iterate)するものである。といっても何が何だか分からないので、具体的な例で見てみることにする。
次のような関数を考える。
// 区間[first, last)に存在する要素をresultにコピーする。 void Copy(int* first, int* last, int* result) { while (first != last) { *result = *first; ++first; ++result; } }
単にint型の配列の要素をコピーするだけの関数である。firstやlastの型は本来constにすべきだが後の説明の都合により、ここではconstを付けない。*1
さて、次のような単方向リストがあるとする。
struct Node { Node* next; int data; };
このリストに対してCopyのような関数を書くとすると次のようになる。
void CopyList(Node* first, Node* last, Node* result) { while (first != last) { result->data = first->data; first = first->next; result = result->next; } }
上のCopy関数とほとんど同じように書ける。違いは
- int*の代わりにNode*を使う
- *pの代わりにp->dataを使う
- ++pの代わりにp = p->nextを使う
の3つだ。では、この3つを配列とリストで揃えてやれば、同じ関数を配列とリストに対して適用できそうだ。
そこで次のようなクラスを定義する。
class ListIterator { Node* p; public: ListIterator(Node* node): p(node) {} int& operator*() { return p->data; } ListIterator& operator++() { p = p->next; return *this; } bool operator!=(const ListIterator& rhs) const { return p != rhs.p; } };
これを使ってCopyListを次のように書きなおす。
void CopyList(ListIterator first, ListIterator last, ListIterator result) { while (first != last) { *result = *first; ++first; ++result; } }
ListIteratorは、p->dataやp = p->nextの形から*p、++pの形にもっていく役割を果たしている。これで上の3つの違いの内、2つは解決できた。残ったひとつはテンプレートを使って解決できる。
template <typename Iterator> void Copy(Iterator first, Iterator last, Iterator result) { while (first != last) { *result = *first; ++first; ++result; } }
これで配列にもリストにも適用できるCopy関数が手に入った。
// array1の先頭から10要素をarray2にコピーする Copy(array1, array1 + 10, array2); // first_nodeからlast_nodeまでをlist2にコピーする Copy(first_node, last_node, list2);
ところで、このCopy関数はさらに一般化することができる。first, lastの型とresultの型が同じである必要はないからだ。
template <typename Iterator1, typename Iterator2> void Copy(Iterator1 first, Iterator1 last, Iterator2 result) { while (first != last) { *result = *first; ++first; ++result; } }
こうしておけば、配列からリストに要素をコピーしたり、逆に、リストから配列に要素をコピーするといったことができる。
さらに、イテレータが必ずしもコンテナの要素をiterateしなければならないわけではない。例えば、次のような変なイテレータ作ることができる。
class EvenIterator { int n; public: EvenIterator(int n0): n(n0) {} int& operator*() { return n; } EvenIterator& operator++() { n += 2; return *this; } bool operator!=(const EvenIterator& rhs) const { return n != rhs.n; } };
そして、次のように呼び出すと
Copy(EvenIterator(0), EvenIterator(14), array);
arrayには 0, 2, 4, 6, 8, 10, 12 が代入される。意味のない例に見えるかもしれないが、状況によってはイテレータのこういった使い方が便利なときもある。実際に、C++の標準ライブラリにはistream_iteratorやostream_iteratorというものがあって、これらは入出力ストリームをイテレータ化したものである。
このように、ListIteratorのようなイテレータを用意してやれば、次の要素に移動できて、要素を参照できて、要素に代入できる集合ならなんでも同じ関数を使うことができる。
実はここで述べてきたイテレータはForwardIteratorという種類のもので、他にも--ができるBidirectionalIteratorや、ランダムアクセスができるRandomAccessIteratorなどがある。さらに、代入ができないInputIteratorや、参照のできないOutputIteratorのように、ForwardIteratorよりも制限のきついイテレータもある。
最後に、ここに挙げたコードがちゃんと動くかどうかは確認していないので、もし動かないことを発見した人がいればこっそり教えてほしい。