Rokiのチラ裏

学生による学習のログ

Range-based for statements with initializer

先日、米国のニューメキシコ州アルバカーキで開催された ISO C++ 委員会による国際会議にて C++20 のドラフトに追加された Range-based for statements with initializer (p0614) についてのメモ。当エントリー内容は同提案書である p0614 に基づく。C++17 では if 文でも、 condition の前に  init-statement を書けるようになったが、それと似たようにして、C++20 では*1 Range-based for の  for-range-declaration の前に  init-statement が書けるようになる。p0614r0 から引用。

Insert a new paragraph at the end of subsection [stmt.ranged].
A range-based for statement of the form
for ( init-statement for-range-declaration : for-range-initializer ) statement
is equivalent to:
{
  init-statement
  for ( for-range-declaration : for-range-initializer ) statement
}

この構文が提案された動機付けの殆どは p0305 で述べられている。本文では、 for-range-declaration type-specifierauto& であり、 for-range-initializer 内でテンポラリオブジェクトが生成された場合、ライフタイムは延長されず結果として dangling reference となってしまうため、これを避ける用途としても利用できると述べられている。p0614r0 から引用。

For example, consider the following simple type that exposes an internal collection:
class T {
  std::vector<int> data_;
public:
  std::vector<int>& items() { return data_; }
  // ...
};
Consider further a function returning a prvalue:
T foo();
Even users who are familiar with the intricacies of prvalue lifetime extension, and who would be confident about a hypothetical statement
for (auto& x : foo()) { /* ... */ }
can easily fail to spot that the similar looking
for (auto& x : foo().items()) { /* ... */ }
has undefined behaviour. While this particular pitfall will presumably stay with us for the foreseeable future (but see below for further discussion), the proposed new syntax will at least allow users to write correct code that looks almost as concise and local as the wrong code above:
for (T thing = foo(); auto& x : thing.items()) { /* ... */ }
Note that we are not proposing that the init-statement be in the same declarative region as any later part of the statement. In other words, for (auto x = f(); auto x : x) is valid and the outer x is hidden in the loop body. This is consistent with the proposed rewrite rule; in the current standard, T x; for (auto x : x) is already valid.

また、上記のようにテンポラリオブジェクトの生成によって dangling reference を持ってしまう問題は Range TS や Boost Range 、その他パイプラインで繋げるような range adaptors の形式をとっているアルゴリズムセットの動作に、直接的に影響を及ぼす*2

for (auto& x : f().filter(pred).top(10).reverse()) { // how many temporaries?
  mutate(&x);
}

range expression 内の全ての一時的な値がループの終わりまで生き残る特有のセマンティックスを与える事で dangling を防ぐという手段も考案されているようだが、この提案はそれとは異なった方法で解決を試みる事ができるとしている。事実、以下のように記述すれば、ループの終わりまでオブジェクトは生き残る。

for (T container = f(); auto& x : container.filter(pred).top(10).reverse()) {
  mutate(&x);                  // ^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^ ^^^^^^^^^
}                              // lvalue    prvalue      prvalue prvalue

なおこの変更による影響についても、同提案内で言及されている。まず標準に対する影響であるが、これは主要な言語拡張であるので、すなわち現在の標準では不正な構文であるので、この変更そのものが標準ライブラリなどに対する設計に影響を与える可能性は低いと述べられている。次にこの構文の実装に対してであるが、様々な実装者が、この提案は一定の実装上の課題を提起するかもしれないと報告したが、原則的かつ合理的に現実で実行可能でなければならない。また、新しい構文では、range-based を通常の for 文と区別するのが難しくなり、2つを区別するためにより洗練された解析が必要になるだろうと述べられている。また 2015 Kona 会議で発表された p0026 multi-range iteration を踏まえて継続的に設計が必要である旨を述べている。multi-range iteration とは、

for (auto x : a; auto y : b; auto z : c) {
  f(a, b, c);
}

というように記述し、範囲 a, b, c を lock-step, すなわち zip の順序で繰り返す事のできる拡張構文である。現在この提案については進展しておらず、不等長の範囲の扱い方などに関する技術的問題を抱えているが、それはライブラリで解決できる問題であるとのこと。Range-based for statements with initializer は、この提案と互換性があるため p0026 が採択された場合においても多大な変更を伴う必要はないとのこと。例えば、以下のようにサポートできるとのこと。

for (T thing = f(); auto x : a; auto y : b; auto z : c) {
  thing.bar(a, b, c);
}

最後に今後の方向性が述べられており、Range-based for にインクリメント機能をつける(for(init; auto x:e; inc))というものや、do ... while 文にて初期化文を書けるようにするといったものが述べられている(do (init;) statement while (expr);)。

*1:執筆時

*2:この問題は CWG 900, CWG 1498にて取り上げられている。