Closed yohhoy closed 5 months ago
とくに制限はないように見えます。
P2644R0の記述は
The fourth context is when a temporary object is created in the for-range-initializer of a range based for statement. Such a temporary object persists until the completion of the statement.
で、(おそらく)当時参考にしたページ(https://timsong-cpp.github.io/cppwp/class.temporary )は
The fourth context is when a temporary object other than a function parameter object is created in the for-range-initializer of a range-based for statement. If such a temporary object would otherwise be destroyed at the end of the for-range-initializer full-expression, the object persists for the lifetime of the reference initialized by the for-range-initializer.
となっていて、執筆時に後者の記述をそのように解釈したものと思われます。ただ、この変更はC++23の範囲内で行われたのか、C++26相当なのかは考えていませんでした。
NBコメントとP2644で提案されていた内容は最終的に文言のみ分離されてP2718R0が規格に取り込まれているようです。
一時オブジェクトが関数の引数の場合
については、P2718では確かに関数パラメータの一時オブジェクトを除いて、とあります。P2644に対するCWGのガイダンスを受けての修正、のようにあるので、これは意図的なものであると思われます。P2718はC++23のNBコメント解決のためのものなので、対象はC++23になるはず。
このサンプルとしては、P2718で追加されているこちらの例が一番分かりやすい(分かりやすいとは言ってない)かと
using T = std::list<int>;
const T& f1(const T& t) { return t; }
const T& f2(T t) { return t; }
T g();
void foo() {
for (auto e : f1(g())) {} // OK, lifetime of return value of g() extended
for (auto e : f2(g())) {} // undefined behavior
}
説明しづらいですが、関数の引数に渡す一時オブジェクトではなく、関数の引数で生成される一時オブジェクトが生存期間延長の例外になっているようです(理由はよくわかりませんが・・・)。
現在の記述は間違ってはいないにしても誤解を招く表現かもしれません。
一時オブジェクトの(この規定が適用されない場合の)寿命が for-range-initializer 完全式の終わりではない場合
については、P2644/P2718の対象となる一時オブジェクトそのものの条件で、文脈的には明らかですけど間違ってはいないかと思います。
このサンプルとしては、P2718で追加されているこちらの例が一番分かりやすい(分かりやすいとは言ってない)かと
using T = std::list<int>; const T& f1(const T& t) { return t; } const T& f2(T t) { return t; } T g(); void foo() { for (auto e : f1(g())) {} // OK, lifetime of return value of g() extended for (auto e : f2(g())) {} // undefined behavior }
まさに当該箇所に関して、StackOverflowの質問 Are function parameter objects temporary objects? への回答で言及されていました。
So the "other than a function parameter object" wording is referring to the parameter
t
off2
.
この回答解釈に従えば、f2
はRange-based forでなくともUBを引き起こすケースですから、"a temporary object other than a function parameter object" と明示的に除外されているのも納得感があります。
よかったです、ただ、このサンプルコードを盛り込むなどして、分かりやすくしたほうがいいですね。
本件、 @tetsurom さんのオリジナル説明に致命的な誤りはなく、私(yohhoy)の誤解によるものと理解しています。大変失礼しました。
現在の記述は間違ってはいないにしても誤解を招く表現かもしれません。
@onihusube さんが指摘するように、疑問を抱いた一因は「一時オブジェクトが関数の引数」を例示サンプルでいう「f1
/f2
関数の実引数に与えるg()
戻り値としての一時オブジェクト」と誤解釈したためでした。
このサンプルコードを盛り込むなどして、分かりやすくしたほうがいいですね。
サンプルコードの引用、賛成です。ただ、補足説明がないとかなり難解な例示コードですね...
using T = std::list<int>;
const T& f1(const T& t) { return t; }
const T& f2(T t) { return t; }
T g();
void foo() {
for (auto e : f1(g())) {} // OK, lifetime of return value of g() extended
for (auto e : f2(g())) {} // undefined behavior
}
この例について少し考えていましたが、本質的にはf2
のローカル変数(関数引数)t
への参照を返しているのがUBの原因で、(一般的に)一時オブジェクトと呼ばれるものは生成されていない気もしてきました。g() -> t
はコピー省略によって直接構築されるはずで、それはf2()
内では立派な左辺値です。
このようなf2()
の呼び出し時に作成されるローカルオブジェクトの事を外から見て一時オブジェクトと呼ぶのかはわかりませんが。むずかしいですね・・・
本質的には
f2
のローカル変数(関数引数)t
への参照を返しているのがUBの原因で、(一般的に)一時オブジェクトと呼ばれるものは生成されていない このようなf2()
の呼び出し時に作成されるローカルオブジェクトの事を外から見て一時オブジェクトと呼ぶ
StackOverflowの回答を信頼するなら、厳密には一時オブジェクト(temporary object)ではないもののCWG意図は上記解釈の通り、となりそうです。
The fourth context is when a temporary object other than a function parameter object is created in the for-range-initializer of a range-based for statement. If such a temporary object would otherwise be destroyed at the end of the for-range-initializer full-expression, the object persists for the lifetime of the reference initialized by the for-range-initializer.
仮に第1センテンスが "a temporary object other than a function parameter object is created in the for-range-initializer of a range-based for statement." では、Range-based for構文の式 for-range-initializer に起因して生じる全ての一時オブジェクト(例示f2
関数内部の引数t
を含む)が延命されることになってしまうため、言語仕様定義のWordingとしては現在の難解な言い回しになっている、のかなと思いました。
記事の参照欄に、対応する提案文書も書いておいていただければ。 https://cpprefjp.github.io/lang/cpp23/lifetime_extension_in_range_based_for_loop.html
a199a4943b749cbfa776d0ff75a77bd6ec885fea 時点で記載のある P2718R0 Annex C 引用コードは、C++20/C++23で挙動が変わってしまうエッジケース例示というニュアンスが強いので、読者の混乱をさけるために削除したほうが良いと思いました。いかがでしょう。
範囲for文の危険性を減らすだけではなく、この仕様はRAIIのためのオブジェクトを無名で作るのに使うことができる。
// P2718R0より引用 void f() { std::vector<int> v = { 42, 17, 13 }; std::mutex m; for (int x : static_cast<void>(std::lock_guard<std::mutex>(m)), v) { ... } }
ここでは、カンマ演算子を活用して実際にイテレートする範囲とは別にロックを獲得している。 この一時オブジェクトは
for-range-initializer
の中で生じているから、範囲for文の終わりまでロックを維持できる。
上記以外の加筆・修正頂いた内容については、追加コメントはありません。
Editorialな指摘として 「議論:」以降のMarkdownレンダリングが崩れているようです。
確認ありがとうございます。確かに、積極的に使っていくものではなさそうな印象だったので、削除しました。
C++23言語機能「範囲for文が範囲初期化子内で生じた一時オブジェクトを延命することを規定」(#1111) の説明にある、一時オブジェクト延命の対象外となる例外条項に疑問があります。
提案文書 P2644R0 を読む限り、上記記載のような例外は存在せず for-range-initializer で生成された全ての一時オブジェクトはfor文末尾まで延命されるように思えます。
cc: @tetsurom さん