イテレータとは実際に何なのか
すべての標準コンテナ――vector、string、map、set、list――は、内部での要素の格納方法がそれぞれ異なります。vector は連続したブロック、map は平衡木、list は連結ノードです。それでも、これらすべてを同じやり方でループ処理できます。それを可能にしているのがイテレータです。1つの要素を「指し示し」、次の要素へどう進むかを知っている小さなオブジェクトです。
イテレータは汎用化されたポインタだと考えてください。begin() から1つ取得し、それが指す要素を * で読み、++ で前へ進めます。各部品は次のように噛み合います。
v.begin() は最初の要素へのイテレータを返し、*it はその要素を返し、++it は次へ進みます。この3つ組――参照外し、前進、比較――が思考モデルのすべてです。
begin()、end()、そして半開区間
もう半分の要素が end() です。重要なのは、end() は最後の要素を指すのではなく、最後の要素の1つ後ろの位置を指す、という点です。これは意図的な「半開」区間 [begin, end) です。begin は含まれ、end は停止の合図です。
この設計のおかげで標準的なループはすっきりします――イテレータが end() と等しくなるまで進めばよいのです。
it < v.end() ではなく it != v.end() であることに注意してください。ほとんどのコンテナのイテレータ(map や list など)は < をサポートせず、== と != のみをサポートするため、!= が移植性のある選択肢です。また auto キーワードを使えば vector<int>::iterator を手で書かずに済みます――コンパイラが型を推論してくれます。
空のコンテナのケースは自然に処理されます。コンテナが空のとき begin() == end() となるため、ループ本体は一度も実行されません。特別扱いは不要です。
end()を絶対に参照外ししない
最もよくあるイテレータのバグは end() の参照外しです。end() は最後の要素の1つ後ろを指すため、*v.end() は自分のものではないメモリを読み出します――未定義動作です。つまり、親切なエラーではなく、クラッシュか静かなゴミ値のどちらかになります。
vector<int> v = {1, 2, 3};
cout << *v.end(); // 未定義動作 - end() は要素ではない
同じ罠は検索関数にも潜んでいます。std::find は値が見つからないときに end() を返すため、参照外しの前に必ずチェックしなければなりません。
返されたイテレータは、参照外しする前に必ず end() と比較してください。この if を忘れることは、初心者のSTLコードでクラッシュを引き起こす最も頻繁な原因の1つです。
const、cbegin、そしてreverseイテレータ
コンテナは、必要に応じてさまざまな種類のイテレータを提供します。
begin()/end()― 通常の読み書きイテレータ(*it = ...が機能する)。cbegin()/cend()―const_iterator。これらを通して読むことはできますが、要素を変更することはできません。rbegin()/rend()― 末尾から先頭へたどる reverse イテレータ。++は実際には後ろ向きに動きます。
reverse イテレータは、面倒な添字計算なしで逆順にループするためのきれいな方法です。
reverse イテレータでも、進めるためには変わらず ++it と書きます――「後ろ向き」の方向はイテレータが内部で処理してくれます。ループが読み取りだけを行うべき場合は cbegin()/cend()(またはコンテナへの const 参照)を使い、うっかり書き込んでしまうのをコンパイラに防いでもらいましょう。
mapのイテレータはpairを返す
すべてのイテレータがポインタの薄いラッパーというわけではありません。std::map のイテレータは木構造をたどり、参照外しするとキーと値のペアである std::pair が得られ、->first と ->second でアクセスします(ポインタと同じく、イテレータも -> をサポートします)。
範囲ベースのforループは begin()/end() の上に直接構築されているため、単純な前向きの反復には通常そちらを使うことになります。明示的なイテレータが真価を発揮するのは、逆順の走査、要素の位置、あるいはアルゴリズムへ範囲を渡したい場合です。
最大の落とし穴:イテレータの無効化
これは、いずれ誰もが痛い目に遭う落とし穴です。コンテナの構造を変更すると、既存のイテレータは無効化されることがあります――解放あるいは移動されたメモリを指してしまうのです。それを使うのは未定義動作です。
vectorの場合、push_back は要素を増やすためにバッファ全体を再確保することがあり、未処理のすべてのイテレータを無効化します。ループ中の削除はさらに悪名高く――これは典型的なクラッシュです。
vector<int> v = {1, 2, 3, 4};
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it % 2 == 0)
v.erase(it); // バグ - erase が it を無効化し、その後の ++it は未定義動作
}
解決策は、erase が削除した要素の次の要素への有効なイテレータを返すことにあります。削除しなかったときだけ前進させましょう。
for のヘッダーに ++it がないことに注目してください――前進するかどうかは本体が決めます。(実際のコードでは、erase-remove イディオムやC++20の std::erase_if がこれを1行で行います。)覚えておくべきルールはこうです。要素を追加または削除するあらゆる操作はイテレータを無効化しうるので、そのような変更をまたいで古いイテレータを保持してはいけません。
次へ:アルゴリズム
範囲を begin/end のペアとして表現できるようになった今、あなたはSTLのアルゴリズムライブラリ全体を手に入れました。sort、find、count、accumulate といった関数は、手元のコンテナが何であるかを気にしません――イテレータの範囲に対して動作するため、同じ呼び出しが vector でも、配列でも、その一部分でも機能します。次は、これらのイテレータを実際に働かせ、ループを標準ライブラリに任せてみましょう。
よくある質問
C++のイテレータとは何ですか?
イテレータとは、コンテナ内の要素を指し示し、次の要素へ移動する方法を知っているオブジェクトです。最初の要素は container.begin() で、末尾の1つ後ろを示すマーカーは container.end() で取得します。*it で参照外しして要素を読み書きし、++it で前進させます。イテレータは、STLのアルゴリズムがあらゆるコンテナで動作することを可能にする共通インターフェースです。
C++におけるイテレータとポインタの違いは何ですか?
vector や配列の場合、イテレータはほぼポインタとまったく同じように振る舞います――* で参照外しし、++ で前進し、==/!= で比較します。しかしイテレータは概念であり、必ずしも生のポインタではありません。map や list のイテレータは木構造や連結ノードをたどるため、* と ++ をオーバーロードするクラス型です。ポインタはイテレータの一種であり、イテレータはその考え方をあらゆるコンテナへ一般化したものです。
C++でイテレータの無効化を引き起こす原因は何ですか?
コンテナの構造を変更すると、既存のイテレータが解放済みあるいは移動済みのメモリを指したまま残ることがあります。vector の場合、push_back は再確保を行ってすべてのイテレータを無効化することがあり、erase は削除した要素とそれ以降のイテレータを無効化します。無効化されたイテレータを使うのは未定義動作です。安全を保つには、erase が返すイテレータを使うか、あらかじめ容量を確保しておきましょう。