std::sort(v.begin(), v.end()) の begin() と end() ― それがイテレータです。ポインタを一般化した「コンテナの要素を指す印」で、STL アルゴリズムすべての基礎。ここが腑に落ちると、std::find、std::transform、std::accumulate が全部同じパターンで書けることが見えてきます。可視化アニメで「歩く印」の動きを体感しましょう。
v.begin() は最初の要素、v.end() は最後の要素の次*it で値、++it で次へconst_iterator や v.cbegin()[begin, end) の美しさstd::find / std::distancestd::advance / std::prev / std::nextイテレータは「コンテナのどこか 1 か所を指す印」です。C のポインタをイメージしてください。++ で次の要素に進み、* で指している値を取り出します。下のデモで実際に動かしてみましょう。
*it で値の読み書き、++it で次へv.begin() が先頭、v.end() は末尾の次(要注意)Q. ポインタとどう違うの?
→ vector の場合、実際ほぼポインタと同じです(内部でポインタで実装されている)。ただ、std::map や std::list のようにメモリが連続していないコンテナでも統一的に使えるのがイテレータの強み。「歩き方を抽象化したポインタ」と思ってください。
Q. なぜ end() は「末尾の次」なの?
→ ループを while (it != end) で書きやすくするため。半開区間 [begin, end) は数学的にも実務的にも扱いやすい設計です。§2 で詳しく。
Q. 範囲 for(for (auto x : v))があればイテレータ要らない?
→ 日常の読み書きループは範囲 for で OK。でも STL アルゴリズム(std::sort、std::find など)を使うときは必ずイテレータを渡します。覚えておけば応用が利く機能です。
イテレータの最大の特徴は「範囲は [begin, end) の半開区間」で表すこと。begin は含む、end は含まない。
end() が「実在しない 1 つ先」を指すおかげで、以下の便利さが生まれます:
begin() == end() で自然に表せるwhile (it != end) と統一できるstd::find は end() を返せる*v.end() は NG。 end が指す先には要素がないので、デリファレンス(*)すると未定義動作。「end の 1 つ前」が最後の有効要素です:*(v.end() - 1) または v.back()。
C でも「配列の末尾」を示すのに、arr + n(最後の要素の 1 つ先のアドレス)を使う慣習がありました:
書き方はほぼ同じ。 イテレータは「ポインタの考え方を、map や list にも使えるよう抽象化したもの」と理解するのが一番分かりやすい。
覚えるべき操作は 5 つだけ。順に見ていきます。
*it ― 値の読み書き++it / it++ ― 次の要素へ前置 ++it を使うのがモダン C++ の流儀。ポインタでは差がないですが、重いイテレータ(map など)では前置のほうが速い場合があります。
it == end / it != end ― 範囲チェックit + n / it - n(ランダムアクセス限定)vector / string / array では +n / -n ができます(ランダムアクセスイテレータ)。list / map ではできないので、std::next(it, n) を使います(§4)。
it->member ― メンバアクセスC のポインタの p->x と同じです。
「読むだけで書き換えない」ことを保証したいとき、const_iterator を使います。書き換えようとするとコンパイルエラーになるので、間違いを未然に防げます。
const_iterator を使う?const std::vector& など)を回すとき → 自動的に const_iteratorcbegin() / cend()const auto& とすれば実質同じ効果 → 実際は範囲 for が多用されるconst は「この変数経由では変更しない」という約束です。コンパイラが守ってくれるので、チームで書く大きなコードで「うっかり書き換え」を防げます。関数引数、メンバ関数、ポインタ/参照…と、いろんな場所で使います。詳しくは STEP 4(第 19 回「メンバ関数と const メンバ」)で深掘り。
イテレータで動く標準関数の代表 3 つを紹介します。
std::find ― 値を探して位置を返す見つかった位置を返し、見つからないときは end() を返すのがイテレータ流。string::find(npos を返す)とは違うので注意。
std::distance ― イテレータ間の距離std::next / std::prev / std::advance ― n だけ進めるvector / string / array では it + n で済みますが、list / map は + が使えないので、これらの関数が汎用的です。
イテレータには「できる操作」のレベルに応じて5 段階のカテゴリがあります。深く理解しなくて OK ですが、「このコンテナは何ができるか」の目安になります。
| カテゴリ | 代表例 | *it 読 | *it 書 | ++it | --it | it+n | it[n] |
|---|---|---|---|---|---|---|---|
| Input | istream_iterator | ◯ | × | ◯ | × | × | × |
| Output | ostream_iterator | × | ◯ | ◯ | × | × | × |
| Forward | forward_list | ◯ | ◯ | ◯ | × | × | × |
| Bidirectional | list / map / set | ◯ | ◯ | ◯ | ◯ | × | × |
| Random Access | vector / string / array | ◯ | ◯ | ◯ | ◯ | ◯ | ◯ |
+n ができない。ストリームは Input / Output で前方にしか進めない。普段 vector ばかり使っているなら、他のコンテナで「あれ、it + n ができない」と気付いたときにこの表を思い出してください。
push_back / insert すると、再確保が起きたタイミングで既存のイテレータはすべて無効になります(ダングリングポインタと同じ問題)。ループ中でコンテナを変更するときは要注意。第 4 回「vector」の罠として扱いました。
イテレータの理解度を 4 問で確認。
std::vector<int> v = {10,20,30}; のとき、v.end() が指す場所は?end() は「末尾の要素の 1 つ先」を指します。*v.end() はデリファレンスしてはいけません(未定義動作)。「末尾の要素」が欲しければ v.back() か *(v.end() - 1)。この半開区間 [begin, end) の設計で、空コンテナでも自然に扱える・ループ終了条件が統一できる、などの利点が生まれます。it が指すのは?std::vector<int> v = {10,20,30,40};
auto it = v.begin();
++it; ++it;v.begin() は 10 を指す。++it で 20、もう一度 ++it で 30。*it は 30 になります。std::find(v.begin(), v.end(), 999) で 999 が見つからなかったとき、戻り値は?end()(存在しない末尾の次)を返すのがイテレータの慣例。if (it != v.end()) でチェックするのが定型。std::string::find が npos を返すのとは別の流儀なので注意。std::list<int> のイテレータで使えない操作は?std::list は双方向イテレータ(Bidirectional)で、+n が使えません(要素が連続していないので)。代わりに std::next(it, 3) や std::advance(it, 3) を使います。vector / string / array はランダムアクセスなので +n が可能。