cpprefjp / site

cpprefjpサイトのMarkdownソース
https://cpprefjp.github.io/
368 stars 152 forks source link

範囲for文範囲初期化子内の一時オブジェクト延命の説明見直し #1246

Closed yohhoy closed 5 months ago

yohhoy commented 5 months ago

C++23言語機能「範囲for文が範囲初期化子内で生じた一時オブジェクトを延命することを規定」(#1111) の説明にある、一時オブジェクト延命の対象外となる例外条項に疑問があります。

ただし、次の場合には適用されない。

  • 一時オブジェクトが関数の引数の場合
  • 一時オブジェクトの(この規定が適用されない場合の)寿命が for-range-initializer 完全式の終わりではない場合

提案文書 P2644R0 を読む限り、上記記載のような例外は存在せず for-range-initializer で生成された全ての一時オブジェクトはfor文末尾まで延命されるように思えます。

cc: @tetsurom さん

faithandbrave commented 5 months ago

とくに制限はないように見えます。

tetsurom commented 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相当なのかは考えていませんでした。

onihusube commented 5 months ago

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の対象となる一時オブジェクトそのものの条件で、文脈的には明らかですけど間違ってはいないかと思います。

yohhoy commented 5 months ago

提案文書 P2644R0 を読む限り(後略)

すみません🙇 最終採択文書の勘違いに端を発していますね。正しくは P2718R0 を参照すべきでした。

yohhoy commented 5 months ago

このサンプルとしては、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 of f2.

この回答解釈に従えば、f2はRange-based forでなくともUBを引き起こすケースですから、"a temporary object other than a function parameter object" と明示的に除外されているのも納得感があります。

tetsurom commented 5 months ago

よかったです、ただ、このサンプルコードを盛り込むなどして、分かりやすくしたほうがいいですね。

yohhoy commented 5 months ago

本件、 @tetsurom さんのオリジナル説明に致命的な誤りはなく、私(yohhoy)の誤解によるものと理解しています。大変失礼しました。

現在の記述は間違ってはいないにしても誤解を招く表現かもしれません。

@onihusube さんが指摘するように、疑問を抱いた一因は「一時オブジェクトが関数の引数」を例示サンプルでいう「f1/f2関数の実引数に与えるg()戻り値としての一時オブジェクト」と誤解釈したためでした。

このサンプルコードを盛り込むなどして、分かりやすくしたほうがいいですね。

サンプルコードの引用、賛成です。ただ、補足説明がないとかなり難解な例示コードですね...

onihusube commented 5 months ago
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()の呼び出し時に作成されるローカルオブジェクトの事を外から見て一時オブジェクトと呼ぶのかはわかりませんが。むずかしいですね・・・

yohhoy commented 5 months ago

本質的には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としては現在の難解な言い回しになっている、のかなと思いました。

faithandbrave commented 5 months ago

記事の参照欄に、対応する提案文書も書いておいていただければ。 https://cpprefjp.github.io/lang/cpp23/lifetime_extension_in_range_based_for_loop.html

yohhoy commented 5 months ago

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レンダリングが崩れているようです。

tetsurom commented 5 months ago

確認ありがとうございます。確かに、積極的に使っていくものではなさそうな印象だったので、削除しました。