📝

AtCoder C++ 範囲for文

2023/07/15に公開

範囲for文とは

範囲for文(The range-based for statement)は、
配列やコンテナなどの複数の要素を持つものから、
すべての要素に含まれる値を順番に取り出して処理するために使われる機能です。
通常のfor文と比べて、簡潔に記述できるメリットがあります。

AtCoderのような競技プログラミングでは、
範囲for文を使うことで、繰り返し処理を効率的に行えることがあります。

本記事ではC++のみに絞って説明しますが、
他の言語にも呼び方の違いはあるものの、範囲for文みたいな機能はあります。
例えば、Javaでは拡張for文とよばれています。
C++では、C++11以降で使えるようになった機能です。

範囲for文の書き方

範囲for文の書き方は以下のようになります。

範囲for文の書き方
for (型 変数名 : 配列やコレクションなどの要素を持つオブジェクト) {
  // ループする処理を表す文
}

範囲for文は、オブジェクトの要素を順番にループ変数に代入してループ処理を実行します。

例1 要素の合計を求めるプログラム

配列aに含まれる要素の合計を求めるプログラムを作りたいとします。
このとき、範囲for文と通常のfor文でどのような違いがでるでしょうか?
それぞれのコードと実行結果を見てみましょう。

まず、範囲for文を使った場合です。
範囲for文は配列の要素を順番に取り出して処理できます。
以下のコードでは、配列aの要素を変数xに代入し、変数sumに加算しています。
最後にsumの値を表示しています。

範囲for文
#include <iostream>
using namespace std;

int main() {
  int a[] = {1, 2, 3, 4, 5}; // 配列aの宣言と初期化
  int sum = 0; // 合計値を格納する変数sumの宣言と初期化
  for (int x : a) { // 範囲for文で配列aの要素を順番にxに代入
    sum += x; // xをsumに加算
  }
  cout << "sum = " << sum << endl; // sumの値を表示
  return 0;
}

次に、通常のfor文を使った場合です。
通常のfor文は繰り返し回数や条件を指定してループすることができます。
以下のコードでは、変数iを0から(配列aの要素数)-1まで変化させています。
その際、配列aのi番目の要素を変数sumに加算しています。最後にsumの値を表示しています。

通常のfor文
#include <iostream>
using namespace std;

int main() {
  int a[] = {1, 2, 3, 4, 5}; // 配列aの宣言と初期化
  int sum = 0; // 合計値を格納する変数sumの宣言と初期化
  for (int i = 0; i < 5; i++) { // for文でiを0から4まで変化させる
    sum += a[i]; // 配列aのi番目の要素をsumに加算
  }
  cout << "sum = " << sum << endl; // sumの値を表示
  return 0;
}

どちらのコードも同じく、以下のような結果が得られます。

sum = 15

しかし、範囲for文はC++で繰り返し処理を行う際に
配列の要素数やインデックスを気にしなくても良いため、
通常のfor文よりもコードがシンプルで読みやすくかけます。

例2 vectorの要素を標準入力から入力するプログラム

次に、vectorの要素を標準入力から入力する処理を行うプログラムで比較してみましょう。
例えば、以下のコードは同じ処理を行います。
比べると、通常のfor文よりもシンプルに記述できることが分かると思います。

通常のfor文
vector<string> v(5);
for (int i = 0; i < v.size(); i++) {
  string& x = v[i];
  cin >> x;
}
拡張for文の書き方
vector<string> v(5);
for (auto& x : v) {
  cin >> x;
}

上記のコードでは、vはvector<string>型のオブジェクトで、文字列の配列を表しています。
したがって、for (auto& x : v)は、
vの各要素(文字列)をxという変数に代入してループするという意味です。
autoは型推論を行うキーワードで、xの型はvの要素の型(string)になります。
&は参照を表す記号で、xがvの要素への参照になることを意味します。
これにより、xを変更するとvの要素も変更されます。

このauto&の部分が理解しにくい思うので、
autoと&の部分に分けて、より詳細に解説していきます。

auto(型推論)とは

今回の例では、型にautoキーワードを指定しています。
autoキーワードは、変数の型を初期化子から推論するために使われます。
つまり、vの要素の型に合わせて変数xの型が決まります。
例えば、仮にもしvがint型の配列なら、xもint型になります。

&(参照)とは

変数名の前には&という記号をつけています。
これは参照という概念で、変数xがvの要素への別名になることを意味します。
参照を使うことで、vの要素を直接変更できます。
参照を使わない場合は、vの要素のコピーが作られてxに代入されるため、
xを変更してもvの要素に影響しません。

参照を使わずに、for (x : v)でもループさせると、xはvの要素のコピーになります。
つまり、xを変更してもvの要素には影響しません。
例えば、以下のコードを実行しても、vの要素は変更されません。

#include <iostream>
#include <vector>
using namespace std;

int main() {
  vector<string> v(5, "World"); //  要素数5、全ての要素を"World"で初期化
  for (auto x : v) {
    cin >> x;
  }
  for (auto s:v) {
    cout << s << " ";
  } //  出力結果:World World World World World 
  return 0;
}

しかし、for (auto& x : v)とすると、xはvの要素の参照になります。
つまり、xを変更するとvの要素も変更されます。
以下のコードを実行すると、vの要素は全て標準入力から受け取った値になります。
つまり、標準入力にa b c d eといれれば、
a b c d eと出力されます。

#include <iostream>
#include <vector>
using namespace std;

int main() {
  vector<string> v(5, "World"); //  要素数5、全ての要素を"World"で初期化
  for (auto& x : v) {
    cin >> x;
  }
  
  for (auto s:v) {
    cout << s << " ";
  }
  return 0;
}
上記の参照のコードの概要を念のためもう一度ざっくりと説明

vector vの要素型と変数xの型がstring型になっています。
また、変数xは宣言時にv[i]で初期化されています。
cinで入力された値をxに代入すると、vector vの要素に反映されます。
このコードでは、string型のvector vを宣言して、その要素数を5としています。
その後、拡張for文でvector vのすべての要素に対して、
標準入力から入力された値を代入しています。
この時、autoキーワードで変数xの型をstring型に推論しています。
また、&記号で変数xをvector vの要素への参照にしています。
これにより、xに代入された値がvector vの要素に反映されます。

まとめ

範囲for文で記述した処理はfor文でも書けます。
どちらがより効率的、かつ適切な処理になるかを考えて使い分けれるといいでしょう。

参考文献

https://cpprefjp.github.io/lang/cpp11/range_based_for.html
https://cpprefjp.github.io/lang/cpp11/auto.html
http://vivi.dyndns.org/tech/cpp/range-for.html

Discussion