この記事では、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-statementoptauto&& 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-expr と end-expr は次のように定まります。
-
for-range-initializer が配列型の式であれば、begin-expr は
range
, end-expr はrange + N
。ただし、Nは配列の要素数で、要素数が不明の場合はエラー -
for-range-initializer がクラス型Cの式で、Cのスコープにおいて名前
begin
とend
が共に見つかれば、begin-expr はrange.begin()
, end-expr はrange.end()
- それ以外の場合、begin-expr は
begin(range)
, end-expr はend(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-expr と end-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のスコープにおいて名前begin
とend
が共に見つかる場合に限り、begin-expr は range.begin()
, end-expr は range.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) {}
-
if
やswitch
の初期化文でも可能です