LoginSignup
13
4

範囲for文の歴史

Last updated at Posted at 2023-12-11

この記事では、C++11で導入された後も改良が続いている範囲for文(range-based for statement)の発展の歴史を解説します。

範囲for文とは

範囲for文はRangeの要素を1つずつ取り出しながらループする繰り返し文です。

C++23における範囲for文の構文は以下の通りです。ただし、opt はその要素が省略できることを表します。

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

これは、以下の通常のfor文を使ったコードと概ね等しくなります。

{
    init-statementopt

    auto&& range = for-range-initializer ;
    auto begin = begin-expr ;
    auto end = end-expr ;
    for ( ; begin != end; ++begin ) {
       for-range-declaration = *begin ;
       statement
    }
}

ここで、

  • init-statement (初期化文): 変数宣言、エイリアス宣言、式文
  • for-range-declaration (for範囲宣言): Rangeの要素を受け取る変数の宣言もしくは構造化束縛
  • for-range-initializer (for範囲初期化子): Rangeを表す式または初期化子リスト

です。また、式 begin-exprend-expr は次のように定まります。

  • for-range-initializer が配列型の式であれば、begin-exprrange, end-exprrange + N。ただし、Nは配列の要素数で、要素数が不明の場合はエラー
  • for-range-initializer がクラス型Cの式で、Cのスコープにおいて名前beginendが共に見つかれば、begin-exprrange.begin(), end-exprrange.end()
  • それ以外の場合、begin-exprbegin(range), end-exprend(range) となり、これらの呼び出しが何を呼び出すかは引数依存の名前探索(ADL)によって決まる

例:

import std;

int main() {
    // {1, 2, 3, 4, 5} の要素を1つずつ取り出し、それぞれxに代入して内部の処理を実行する
    for(auto x : {1, 2, 3, 4, 5}) {
        std::print("{}", x); // => 12345
    }
}

なお、for-range-initializer にて生じた一時オブジェクトは、関数の引数を除いて、範囲for文の終了まで寿命が延長されます。

ところで、begin-exprend-expr の決め方については std::(ranges::)begin/end が配列やクラスのメンバーの場合にも対応しているので、最後の規定だけあれば足りる気がしませんか?
それなのにわざわざ範囲for文の定義でカバーしているのは、おそらく std::(ranges::)begin/end を経由するオーバーヘッドを(実際にはインライン展開される可能性が高いとしても)避けるためではないかと思われます。

メンバー begin, end は、関数呼び出し演算子を適用できれば関数でなくても構いません。

C++11

C++11では範囲for文が初めて導入されました。

当時の範囲for文は以下の構文でした。

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, end は一度に定義されるため、同じ型に限られていました。

また、一時オブジェクトの寿命に関する特別ルールも存在せず、範囲の寿命が尽きないように注意が必要でした。

#include <vector>
#include <string>
#include <iostream>

std::vector<std::string> getstr() {
  return {"hello", "UB"};
}

int main()
{
  for(auto&& c : getstr()[0]) { // ダングリング参照が発生
    std::cout << c << std::endl; // ダングリング参照にアクセスし未定義動作
  }
}

C++11で導入された初期化子リストは初めから for-range-initializer で利用可能でした。

C++14

C++14では範囲for文の仕様は変わっていません。

C++17

C++17では、イテレーターの型と異なる型の終端イテレーター(番兵ともいう)を許容するようになりました。

  • C++20のレンジライブラリでは、範囲for文における取り扱いだけでなく、一般にイテレーターと番兵の型は異なるものとして再定義されています

また、C++17で導入された構造化束縛が for-range-declaration において使えるようになっています。

C++20

C++20では、 for-range-initializer がクラス型Cの式で、Cのスコープにおいて名前beginend共に見つかる場合に限り、begin-exprrange.begin(), end-exprrange.end() となるように変更されました。

それまではどちらか一方だけ見つかればOKでしたが、begin / end というメンバーをすでに持っていて、しかもそれが範囲for文で使われることを意図していないクラスに対して範囲for文を使えない原因になっていました。

  • この変更はC++11に対する修正となっているため、使用する規格のバージョンをC++11にしていても適用されます

また、範囲for文でも初期化文を書けるようになりました。具体的には init-statementopt の部分がこのとき追加されています。

  • C++17でif文/switch文に初期化文を書けるようになり、それとの一貫性を持たせるため。また、C++20時点では、一時オブジェクトの寿命を延ばすテクニックとしても有効でした

C++23

C++23では、for-range-initializer にて生じた一時オブジェクトは、範囲for文の終了まで寿命が延長されることが定められ、範囲for文はより安全に使えるようになりました。

これを通常のfor文に展開した後のコードでどう実現するかは規定されていませんが、例えば以下のように変数を追加すれば可能です。

int main()
{
  // for(auto&& c : getstr()[0]) { // ダングリング参照が発生
  //  std::cout << c << std::endl; // ダングリング参照にアクセスし未定義動作
  // }
  {
    auto&& __expr0 = getstr();
    auto&& __range = expr0[0];
    auto __begin = expr1.begin();
    auto __end = expr1.end();
    for ( ; __begin != __end; ++__begin ) {
      auto&& c = *__begin;
      std::cout << c << std::endl;
    }
  }
}

また、初期化文でエイリアス宣言ができるようになりました(元々ここでtypedefは書けたので一貫性のため)。

for (using T = int; T e : v) {}
  • ifswitch の初期化文でも可能です

参考リンク

13
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
4