「各要素に対してこれを行う」
従来のカウンタ式 for ループは、駆動するインデックスがあるときには優れています。しかしたいていの場合、インデックス自体には関心がなく、ただコンテナのすべての要素に触れたいだけです。そのために for (int i = 0; i < v.size(); i++) と書くのはうるさく、i はオフバイワンのミス一つで範囲外を読み出してしまいます。
C++11 はまさにこのために範囲ベース for ループを追加しました。変数に名前を付け、コンテナを指し示すと、ループがすべての要素を巡ってくれます。
インデックスも .size() も、間違えやすい境界もありません。「scores の中の各 s について」と読んでください。生の配列、std::vector、std::string、std::map、そして begin() と end() を提供するものなら何にでも使えます。
型は auto に選ばせる
要素の型を明示的に書くのは機能しますが、もろいやり方です。コンテナの型を変えると、すべてのループも変えなければなりません。範囲ベースの for を auto と組み合わせれば、コンパイラが要素の型を推論してくれます。
ただし、ここには隠れたコストがあります。素の auto name は string を推論し、毎回各要素を name にコピーします。int ならコストはありませんが、string や大きな構造体では反復ごとに無駄なアロケーションが発生します。その解決策が参照であり、次に理解すべき事柄です。
auto& でその場で変更する
auto x と書くとコピーが得られるので、x への代入はコピーを変えるだけで、コンテナは変わりません。この落とし穴に注意してください。
n は使い捨てのコピーなので、10 倍する処理は静かに何もしません。実際に要素を編集するには、auto& で参照によって受け取ります。
たった一つの & が、「見るだけで触らない」と「その場で編集する」の違いそのものです。変更が消えてしまう理由を不思議に思ったら、ほぼ必ずこれが原因です。
コピーせずに読む: const auto&
要素を読むだけでよいのにコピーが高くつく場合は、const auto& を使います。参照によってコピーを避け、const は何も変更しないことを文書化(かつ強制)します。
良い目安はこうです。
for (auto x : c) // コピー - 安価な型 (int, char, ポインタ)
for (auto& x : c) // 編集 - 要素を変更したい
for (const auto& x : c) // 読み取り - 参照するだけの重い型
読むときは既定で const auto& を、書くときは auto& を使いましょう。素の auto は、本当に小さくコピーが安価な型のときだけにとどめてください。
map と pair を巡回する
std::map に対する範囲ベースの for は、各エントリについて .first(キー)と .second(値)を持つ std::pair を渡してくれます。C++17 以降、構造化束縛を使えば、その pair をループのヘッダーでそのまま 2 つの名前付き変数に分解できます。
[name, age] は、あちこちで entry.first と entry.second を繰り返すよりもはるかに明確です。ここでも const auto& を保ってください。map エントリのキーは string なので、各 pair をコピーすると無駄になります。
落とし穴: ループ中にサイズを変えない
最大の落とし穴は、範囲ベースの for が巡回している最中にコンテナのサイズを変えることです。push_back、erase、insert、clear を呼び出すと、内部のストレージが再確保され、ループ内部のイテレータが無効化されることがあります。結果は未定義動作、つまり親切なエラーではなくクラッシュやゴミデータです。
vector<int> v = {1, 2, 3};
for (int x : v) {
v.push_back(x); // 未定義動作 - 再確保が範囲を無効化する
}
処理中に要素を追加・削除する必要がある場合は、インデックスベースまたはイテレータベースの for ループに切り替えて境界を自分で管理するか、別の結果用コンテナを作って後で入れ替えてください。同じ系統のより小さな落とし穴が二つあります。範囲ベースの for を、すぐに寿命が尽きる一時オブジェクトに束縛してはいけません(for (auto x : makeVector()) は問題ありませんが、for (auto& x : someObj.getTempVector()) はダングリングになり得ます)。そして、for (auto& c : myString) を使えば個々の文字をその場で変更できることを覚えておきましょう。
次: 関数
範囲ベースの for ループは反復を整理してくれます。そしてたった今学んだ auto / auto& / const auto& の選択は、C++ で最も重要なツールの一つにそのまま受け継がれます。次は、ロジックを再利用可能な関数にまとめます。コードに名前・引数・戻り値を与えることで、同じことを繰り返す代わりに、どこからでも呼び出せるようにします。
よくある質問
C++ の範囲ベース for ループとは何ですか?
範囲ベースの for ループは、インデックスやイテレータを自分で管理しなくても、コンテナ(配列、vector、string、map など)のすべての要素を巡回します。構文は for (auto x : container) { ... } です。C++11 で追加され、「各要素に対してこれを行う」と表現する最もすっきりした方法です。
範囲ベース for ループで auto ではなく auto& を使うべきなのはいつですか?
要素をその場で変更したいときは auto& x を、読み取るだけでコピーを避けたいとき(string、vector、大きなオブジェクトでは重要)は const auto& x を使います。素の auto x は反復ごとにコピーを作ります。int のような安価な型なら問題ありませんが、それ以外では無駄になります。
C++ では範囲ベース for ループの中で vector のサイズを変更できますか?
いいえ。反復中のコンテナに対して push_back、erase、insert、clear を呼び出すと、ループ内部のイテレータが無効化され、未定義動作になります。クラッシュしたり、データを静かに壊したりすることがあります。ループ中に要素を追加・削除する必要がある場合は、代わりにインデックスベースまたはイテレータベースの for ループを使ってください。