cpprefjp / site

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

feat: P0588R1で追加されたodr-usableについて記述 #1103

Open yumetodo opened 1 year ago

yumetodo commented 1 year ago

ref:

疑問点

void f(int n) {
  [=](int k = n) {};            // error: n is not odr-usable due to being
                                // outside the block scope of the lambda-expression
}

ここでnがodr-usableではない理由が理解できていないです。

関数fの定義スコープはnの宣言領域に含まれるはずです。またcapureは=なのでdefault-capureです。

コメントではoutside the block scope of the lambda-expressionとあるのですが、lambda-expressionのblock scopeとはどこでしょうか? 言い換えると↓の部分をどう解釈したらいいかわかっていません。

https://timsong-cpp.github.io/cppwp/n4861/basic.def.odr#9.2.2

and the block scope of the lambda-expression is also an intervening declarative region.

https://timsong-cpp.github.io/cppwp/n4861/expr.prim.lambda

block scopeでgrepしましたがそれらしい文面がありませんでした。

yohhoy commented 1 year ago

"block scope"の定義はラムダ式固有ではなく、 [basic.scope.block]/p1 に従うのではないでしょうか。

A name declared in a block ([stmt.block]) is local to that block; it has block scope. [...]

[expr.prim.lambda]で定義される構文要素 lambda-expression のうち、 compound-statement つまりラムダ式本体{...}がblock scopeを構成するという解釈です。


本件は CWG 2380 capture-default makes too many references odr-usable で後付け修正された内容に関連するようです。

onihusube commented 1 year ago

まず、P0588R1のやっていることは4つあって

  1. ラムダ式がクラスメンバ初期化子で使用された時の挙動の明確化 (CWG1632)
  2. ラムダ式の構文内でキャプチャした対象に対するdecltype((x))の振る舞いの明確化 (CWG1913)
  3. ラムダ式が名前をキャプチャする(できる)場所の明確化
  4. 構造化束縛をキャプチャできないことを明確化

だと思います。4はほぼオマケです。

大前提としてラムダ式がキャプチャする必要があるものは常にローカルなものです。ここではそれはローカルエンティティとして指定されており、ほぼローカル変数と*thisのことです(非静的メンバ変数はローカルエンティティではありません)。

ここでのodr-usableとはおそらく、ある名前をラムダ式がキャプチャできるのかを言うために導入されており、キャプチャするのかどうか不明瞭だったところを弾く(あるいはキャプチャ範囲を狭める)ためにodr-usableではない場合は不適格、としています。これはCWG2380によって事後的にも制限されています。

従って、odr-usableという概念は最初のやっていることの1と3に関わるものです。

その上で、odr-usableとはまず、ローカルエンティティに対する概念であって

あるローカルエンティティがその宣言領域(シャドウイングされないで名前が有効な領域、スコープ)内で参照される場合、そのエンティティもしくはその場所が

のどちらかに該当しており

そのローカルエンティティが導入される地点とそのローカルエンティティが参照される領域との間に介在している宣言領域のそれぞれについて

のどちらかに該当する場合に、そのローカルエンティティはodr-usableとなります。

介在する宣言領域というのは、ローカルエンティティの導入(宣言/定義)地点から、そのローカルエンティティ(の名前)を参照する地点の間に存在している宣言領域(主に各種スコープのこと)です。介在する(intervening)というのは、参照地点から導入地点の間でそのスコープが重なっている様を言っているのだと思います

前段の条件の2は、P0588R1のやっていることの1に関わるもので、クラスメンバ初期化子と非静的メンバ関数の引数宣言でthisをキャプチャするラムダ式のハンドリングのためだと思われます(これは今回関係ありません)。

P0588R1の中程で、ラムダ式が明示的にキャプチャするもの(ローカルエンティティ)はodr-usableでなければならないとされています(これも今回関係ありません)。

で、このPRのメインの謎であるサンプルコードが含まれる例を見ていくと

void f(int n) {
  [] { n = 1; };                // #1 error: n is not odr-usable due to intervening lambda-expression
  struct A {
    void f() { n = 2; }         // #2 error: n is not odr-usable due to intervening function definition scope
  };
  void g(int = n);              // #3 error: n is not odr-usable due to intervening function parameter scope
  [=](int k = n) {};            // #4 error: n is not odr-usable due to being
                                // outside the block scope of the lambda-expression
  [&] { [n]{ return n; }; };    // #5 OK
}

この例の場合、ローカルエンティティnは関数fの関数パラメータスコープを宣言領域として導入されていて、*thisではないので、odr-usableの前段の条件はクリアしており、問題となるのは後段の条件のみです。

  1. ローカルエンティティnはラムダ式の関数パラメータスコープに囲われていますが、そのラムダ式はキャプチャに何も指定していない(明示的にも暗黙的にもnをキャプチャしていない)ため、この場所でnはodr-usableではありません
  2. ローカルエンティティnA::f()の関数定義スコープとAのクラススコープに囲われています。いずれもブロックスコープではないため(当然ラムダ式の関数パラメータスコープでもないため)、odr-usableではありません
  3. ローカルエンティティng()の関数パラメータスコープに囲われていますが、これも後段2条件のどちらに合致するスコープでもないため、odr-usableではありません
  4. ローカルエンティティnはラムダ式の関数パラメータスコープに囲われていて、そのラムダ式はデフォルトキャプチャを持っています。しかし、そのラムダ式の本体のスコープが介在していない(nが参照される地点は本体の外側の)ため、odr-usableではありません
  5. ローカルエンティティnは2つのラムダ式の関数パラメータスコープに囲われていて、いずれのラムダ式もnをキャプチャしており(デフォルトキャプチャ->明示的キャプチャ)、nが参照される地点は2つのラムダ式の本体のブロックスコープの内部です。従って、これはodr-usableです。

多分このサンプルの言いたいことは、関数ローカル変数を関数の外に持ち出すことができうるケースを厳しく制限(コンパイルエラーに)しているよ、ってことだと思います(感想

このPRの疑問に答えるにはこれで良いと思います、間違ってたらすいません・・・

yumetodo commented 1 year ago

もっと具体的な例を持ってきてみて

auto f()
{
  return [](int n = 3) { return n; };
}
int main()
{
  auto ff = f();
  ff();
  ff(4);
}

こんな感じでlambda式ごと外に持っていけるけどそのとき上の例の= 3が関数fのスコープの変数を使うとかしてたらそんなもんmain関数から見えるわけ無いだろっていう感じですかね・・・。

関数の引数スコープってのがいまいちピンときてなかったのですが、呼び出し側のスコープで考えると理解すればいいのだろうか・・・。

onihusube commented 1 year ago

こんな感じでlambda式ごと外に持っていけるけどそのとき上の例の= 3が関数fのスコープの変数を使うとかしてたらそんなもんmain関数から見えるわけ無いだろっていう感じですかね・・・。

そうですね、多分このサンプルコードが言いたいのはそういうことで、odr-usableはそういう状況を弁別するための概念というか道具だと思います。

もし仮にこれらのサンプルコードが適格だとすると、暗黙の参照キャプチャのような事が行われることになると思うので、それを考えると不適格とされているのはなぜダメで適格なのはなぜ良いのか理解しやすいかなあと思います。

関数の引数スコープってのがいまいちピンときてなかったのですが、呼び出し側のスコープで考えると理解すればいいのだろうか・・・。

ここでのfunction parameter scope(上の投稿では関数パラメータスコープと呼んでいます)とは、単純に関数の引数の名前が(シャドウイングされずに)参照可能なスコープの事です。それは関数引数宣言の点から、その関数の定義の終端までの範囲になります。

void f(
  int a1,  // a1の関数パラメータスコープの開始
  int a2   // a2の関数パラメータスコープの開始
) {
  // a1, a2の関数パラメータスコープの途中

  {
    int a1;  // ブロックスコープ変数a1のスコープの開始
             // 関数引数a1の関数パラメータスコープの中断
  } // ブロックスコープ変数a1のスコープの終了
  // 関数引数a1の関数パラメータスコープの再開

}  // a1, a2の関数パラメータスコープの終了

これは多分、宣言領域(declarative region)の考え方と同じで、関数パラメータスコープとは関数引数の宣言領域の事だと思います。

yohhoy commented 1 year ago

ご参考までに: function parameter scope も [basic.scope.param] で定義されています。

yumetodo commented 1 year ago

なるほど・・・。

PRの内容も再整理が必要そうですね・・・。