site
site copied to clipboard
feat: P0588R1で追加されたodr-usableについて記述
ref:
- #718
- https://timsong-cpp.github.io/cppwp/n4861/basic.def.odr#9
- https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0588r1.html
疑問点
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しましたがそれらしい文面がありませんでした。
"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 で後付け修正された内容に関連するようです。
まず、P0588R1のやっていることは4つあって
- ラムダ式がクラスメンバ初期化子で使用された時の挙動の明確化 (CWG1632)
- ラムダ式の構文内でキャプチャした対象に対する
decltype((x))
の振る舞いの明確化 (CWG1913) - ラムダ式が名前をキャプチャする(できる)場所の明確化
- 構造化束縛をキャプチャできないことを明確化
だと思います。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の前段の条件はクリアしており、問題となるのは後段の条件のみです。
- ローカルエンティティ
n
はラムダ式の関数パラメータスコープに囲われていますが、そのラムダ式はキャプチャに何も指定していない(明示的にも暗黙的にもn
をキャプチャしていない)ため、この場所でn
はodr-usableではありません - ローカルエンティティ
n
はA::f()
の関数定義スコープとA
のクラススコープに囲われています。いずれもブロックスコープではないため(当然ラムダ式の関数パラメータスコープでもないため)、odr-usableではありません - ローカルエンティティ
n
はg()
の関数パラメータスコープに囲われていますが、これも後段2条件のどちらに合致するスコープでもないため、odr-usableではありません - ローカルエンティティ
n
はラムダ式の関数パラメータスコープに囲われていて、そのラムダ式はデフォルトキャプチャを持っています。しかし、そのラムダ式の本体のスコープが介在していない(n
が参照される地点は本体の外側の)ため、odr-usableではありません - ローカルエンティティ
n
は2つのラムダ式の関数パラメータスコープに囲われていて、いずれのラムダ式もn
をキャプチャしており(デフォルトキャプチャ->明示的キャプチャ)、n
が参照される地点は2つのラムダ式の本体のブロックスコープの内部です。従って、これはodr-usableです。
多分このサンプルの言いたいことは、関数ローカル変数を関数の外に持ち出すことができうるケースを厳しく制限(コンパイルエラーに)しているよ、ってことだと思います(感想
このPRの疑問に答えるにはこれで良いと思います、間違ってたらすいません・・・
もっと具体的な例を持ってきてみて
auto f()
{
return [](int n = 3) { return n; };
}
int main()
{
auto ff = f();
ff();
ff(4);
}
こんな感じでlambda式ごと外に持っていけるけどそのとき上の例の= 3
が関数fのスコープの変数を使うとかしてたらそんなもんmain関数から見えるわけ無いだろっていう感じですかね・・・。
関数の引数スコープってのがいまいちピンときてなかったのですが、呼び出し側のスコープで考えると理解すればいいのだろうか・・・。
こんな感じで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)の考え方と同じで、関数パラメータスコープとは関数引数の宣言領域の事だと思います。
ご参考までに: function parameter scope も [basic.scope.param] で定義されています。
なるほど・・・。
PRの内容も再整理が必要そうですね・・・。