最終更新日時(UTC):
が更新

履歴 編集

範囲for文(C++11)

概要

範囲for文(The range-based for statement)は配列やコンテナを簡潔に扱うためのfor文の別表現である。

範囲for文が便利な例として、コンテナの各要素を処理するループを挙げる。

C++03のfor文では以下のように書ける:

std::vector<int> v;

for (std::vector<int>::const_iterator it = v.begin(), e = v.end(); it != e; ++it) {
  std::cout << *it << std::endl;
}

ループ内の処理と直接関係のない変数(イテレータやポインタ)が出現し、ループ条件も加わりfor文が長くなりがちである。

C++11の範囲for文を使うと以下のように書ける:

std::vector<int> v;

for (const auto& e : v) {
  std::cout << e << std::endl;
}

変数宣言には直接コンテナ内の要素の型(上記の例であればconst int& eなど)を書いても良いし、型推論autoを使うと、さらに簡潔に書ける。

変数宣言にconst参照const auto& eを書くとコンテナ内の要素の変更を禁止し、要素のコピーも行わない。参照auto& eを書くと、コンテナ内の要素を変更できる。実体auto eを書くと各要素がコピーコンストラクタによってコピーされてからfor文に渡される。

変数宣言 e を変更可能か? コンテナ内の要素を変更可能か?
const auto& e No No
auto& e Yes Yes
auto e Yes No

仕様

範囲for文は配列または、begin()およびend()で表されるイテレータ範囲に含まれる全ての要素に対して、処理を実行する。

範囲for文は以下の構文を持つ:

for ( for-range-declaration : for-range-initializer ) statement

for-range-declarationには変数宣言を書く。ここで宣言した変数に範囲内の要素が先頭から終端まで順番に代入される。

for-range-initializerにはfor文が処理すべき範囲を表す値を書く。

値の型が配列の場合、配列のサイズが分かるものでなければエラーとなる。値の型が配列以外(クラスなど)の場合、begin()end()イテレータ範囲の先頭と終端が表せるものでなければエラーとなる。

語弊を恐れず言えば、メンバ関数にbegin()およびend()を持つクラスであれば、何でも範囲for文の範囲として指定できる。

従って標準コンテナのみならず、ユーザ定義のクラスに対しても範囲for文を適用可能である。

C++03のfor文と異なりセミコロンではなくコロンで区切ることに注意する。

for文への展開

C++11、C++14において、範囲for文は以下のように通常のfor文へと展開される(C++17以降は展開のされ方が異なる)。

// for ( for-range-declaration : for-range-initializer ) statement
{
  auto && __range = for-range-initializer;
  for ( auto __begin = begin-expr, __end = end-expr;
        __begin != __end;
        ++__begin ) {
    for-range-declaration = *__begin;
    statement
  }
}

展開後に現れる変数名は仮のものであり、実際に変数として見えるわけではない。しかし、デバッガーにこれらの変数が現れることがある。

begin-exprとend-exprの具体的な内容は、イテレータ範囲として何を渡すかによって3通りに分かれる。いずれの場合も、begin-exprとend-exprは同じ型でなければならない。

配列を範囲として渡したとき、以下のように展開される:

{
  auto && __range = for-range-initializer;

  for (auto __begin = __range, __end = __range + __bound; __begin != __end; ++__begin) {
    for-range-declaration = *__begin;

    statement
  }
}

  • ただし、__boundは配列の要素数(要素数が不明な場合はill-formed)。

範囲の型がクラスであって、メンバbeginendが両方存在するとき、以下のように展開される:

{
  auto && __range = for-range-initializer;

  for (auto __begin = __range.begin(), __end = __range.end(); __begin != __end; ++__begin) {
    for-range-declaration = *__begin;

    statement
  }
}

  • メンバbeginendが関数ではない場合でもこのように展開されるが、呼び出しができなければエラーとなる。
  • 当初のC++11では、メンバbeginendが片方しかなくてもこのように展開されていた。これは規格の不具合であり、修正P0962R1がC++11に遡って適用された

範囲の型が配列でもメンバbeginendのいずれかを持つクラスでもないとき、以下のように展開される:

{
  auto && __range = for-range-initializer;

  for (auto __begin = begin(__range), __end = end(__range); __begin != __end; ++__begin) {
    for-range-declaration = *__begin;

    statement
  }
}

  • 展開されたコード内のbegin()end()が正確に何を呼びだすかについては、引数依存の名前探索(argument-dependent name lookup; ADL)を参照のこと。

従って概要で挙げた例は以下のように展開される:

for (const auto& e : vec) {
  std::cout << e << std::endl;
}

{
  auto && __range = vec;

  for (auto __begin = __range.begin(), __end = __range.end(); __begin != __end; ++__begin) {
    const auto& e = *__begin;

    std::cout << e << std::endl;
  }
}

#include <iostream>
#include <vector>

class my_container {
public:
  int *begin() {
    return &buf[0];
  }
  int *end() {
    return &buf[5];
  }

private:
  int buf[5] = {21, 22, 23, 24, 25};
};

int main()
{
  //配列に対して範囲for文を使う
  int array[5] = {1, 2, 3, 4, 5};

  std::cout << "For int[5]: " << std::endl;
  for (auto& e : array) {
    std::cout << "  " << e << std::endl;
  }

  //標準コンテナに対して範囲for文を使う
  std::vector<int> vec = {10, 11, 12, 13};

  std::cout << "For std::vector<int>: " << std::endl;
  for (auto& e : vec) {
    std::cout << "  " << e << std::endl;
  }

  //ユーザ定義のクラスに対して範囲for文を使う
  my_container mc;

  std::cout << "For my_container: " << std::endl;
  for (auto& e : mc) {
    std::cout << "  " << e << std::endl;
  }

  return 0;
}

出力

For int[5]:
  1
  2
  3
  4
  5
For std::vector<int>:
  10
  11
  12
  13
For my_container:
  21
  22
  23
  24
  25

使用上の注意

範囲for文を使う際はイテレータが無効にならないように気をつけなければならない。
なぜなら、範囲for文が展開されたときのfor-range-declaration = *__begin;の部分で無効になったイテレータの間接参照が行われた場合、それは未定義動作だからである。

コンテナの要素追加/削除をしてしまう場合

例えば、範囲for文でまさにイテレートしているコンテナに要素を追加/削除するなどして、イテレータが無効となる場合がある。この場合は範囲for文を使ってはいけない。

#include <vector>
#include <iostream>
int main()
{
  std::vector<int> v{ 5, 5, 0, 5, 1 };
  //v.size() == v.capacity() にする
  v.shrink_to_fit();
  for(auto&& i : v) {
    std::cout << ' ' << i;
    if (5 == i) {
      //要素を追加するとき、capacityを超えるので再アロケーションが発生し、イテレータが無効になる
      v.emplace_back(123);
    }
  }
}

#include <iostream>
#include <string>
#include <unordered_map>
int main()
{
  std::unordered_map<std::string, int> m{
    { "ajjnr", 3 },
    { "kjngs@mgg", 9 },
    { "sdjvnmwb", 12 },
    { "kgf", 64 }
  };

  for(auto&& kv : m) {
    std::cout << kv.first << ',' << kv.second << std::endl;
    if (kv.first.size() < 4) {
      // 現在の要素のkeyを使って削除
      // → 範囲forのイテレート中のイテレータが無効になる
      m.erase(kv.first);
    }
  }
}

for-range-initializerに渡したものの寿命が切れてしまう場合

for-range-initializerに渡したものの寿命が切れてイテレータが無効になるケースもある。

下の例ではsomething { 1,2,3,4,5,6,7,8,9,0 }のようにして生成された一時オブジェクトが__rangeによって束縛されていないため、直ちに寿命が尽きてしまう。このような場合、寿命が切れないようにしなければならない。

#include <initializer_list>
#include <iostream>
#include <vector>

struct something
{
  std::vector<int> v;

  something(const std::initializer_list<int>& l ) : v(l) {}
  std::vector<int>& get_vector() { return v; }
  ~something() noexcept { std::cout << "destructor" << std::endl; }
};

int main()
{
  // get_vectorは内部に持つvectorへの参照を返す
  for( auto e : something { 1,2,3,4,5,6,7,8,9,0 }.get_vector() )
  { // 破棄されたオブジェクトへの参照
    std::cout << e;
  }
  std::cout << std::endl;
}

ただしこのバグはコンテナの参照を返すメンバ関数(上記ではget_vector)に左辺値修飾することで防げる場合もある

#include <initializer_list>
#include <iostream>
#include <vector>

struct something
{
  std::vector<int> v;

  something(const std::initializer_list<int>& l ) : v(l) {}
  std::vector<int>& get_vector() & { return v; }
  // これを実装すれば実行効率を損なわず、安全にいつでもget_vectorを呼び出せる
  //std::vector<int> get_vector() && { return std::move(v); }
  ~something() noexcept { std::cout << "destructor" << std::endl; }
};

int main()
{
  // get_vectorを呼び出せないので不適格→バグに気がつく
  for( auto e : something { 1,2,3,4,5,6,7,8,9,0 }.get_vector() )
  {
    std::cout << e;
  }
  std::cout << std::endl;
}

また、C++23からはfor-range-initializerの寿命が条件を満たせば延長されるようになったので(C++23 範囲for文が範囲初期化子内で生じた一時オブジェクトを延命することを規定)、この問題を踏みにくくなっている。

関連項目

参照