Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

yumetodo
Copy link
Member

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
Copy link
Member

yohhoy commented Jan 10, 2023

"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
Copy link
Member

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

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

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

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

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

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

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

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

  • *thisではない、もしくは
  • クラススコープかラムダ式のものではない関数パラメータスコープに囲われている
    • そのスコープの最も内側のスコープが関数パラメータスコープであるならば、そのスコープは非静的メンバ関数のもの

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

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

  • 介在する宣言領域はブロックスコープである、もしくは
  • 介在する宣言領域はラムダ式の関数パラメータスコープであり
    • そのローカルエンティティを明示的にキャプチャしているか、デフォルトキャプチャを持っていて
    • そのラムダ式のブロックスコープ(本体)もまた、介在する宣言領域である

のどちらかに該当する場合に、そのローカルエンティティは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
Copy link
Member Author

yumetodo commented Jan 11, 2023

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

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

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

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

@onihusube
Copy link
Member

onihusube commented Jan 11, 2023

こんな感じで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
Copy link
Member

yohhoy commented Jan 12, 2023

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

@yumetodo
Copy link
Member Author

なるほど・・・。

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

@yumetodo yumetodo marked this pull request as draft January 12, 2023 13:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants