web-dev-qa-db-ja.com

C ++ 11ラムダでの参照による参照のキャプチャ

このことを考慮:

#include <functional>
#include <iostream>

std::function<void()> make_function(int& x) {
    return [&]{ std::cout << x << std::endl; };
}

int main() {
    int i = 3;
    auto f = make_function(i);
    i = 5;
    f();
}

このプログラムは、未定義の動作を呼び出さずに5を出力することが保証されていますか?

xを値([=])でキャプチャした場合の動作を理解していますが、参照でキャプチャして未定義の動作を呼び出しているかどうかはわかりません。 make_functionが戻った後にぶら下がり参照が発生する可能性がありますか、または元の参照オブジェクトがまだ存在する限り、キャプチャされた参照が機能することが保証されていますか?

ここで最終的な標準ベースの回答を探します:)実際には十分に機能しますこれまで;)

58
Magnus Hoff

コードは機能することが保証されています。

標準の文言を掘り下げる前に、このコードが機能するのはC++委員会の意図です。しかし、現状ではこの文言は十分に明確ではないと考えられていました(実際、C++ 14以降の標準に加えられたバグ修正は、それを機能させる繊細な取り決めを破りました) CWG issue 2011 =問題を明確にするために提起され、現在、委員会を通過しています。私の知る限り、これが間違っている実装はありません。


いくつかのことを明確にしたいと思います。BenVoigtの答えには、混乱を引き起こしている事実上の誤りが含まれているためです。

  1. 「スコープ」は、C++の静的な語彙の概念であり、非修飾名検索が特定の名前を宣言に関連付けるプログラムソースコードの領域を記述します。生涯とは何の関係もありません。 [basic.scope.declarative]/1 を参照してください。
  2. 同様に、ラムダの「到達範囲」ルールは、キャプチャが許可されるタイミングを決定する構文プロパティです。例えば:

    void f(int n) {
      struct A {
        void g() { // reaching scope of lambda starts here
          [&] { int k = n; };
          // ...
    

    nはスコープ内にありますが、ラムダの到達スコープには含まれていないため、キャプチャできません。別の言い方をすれば、ラムダの到達範囲は、変数に到達してキャプチャすることができる「上」の距離です-外側の(非ラムダ)関数とそのパラメータに到達できますが、その外側に到達することはできません。外部に現れる宣言をキャプチャします。

したがって、「スコープに到達する」という概念は、この質問とは無関係です。キャプチャされるエンティティはmake_functionのパラメーターxは、ラムダの到達範囲内にあります。


それでは、この問題に関する規格の文言を見てみましょう。 [expr.prim.lambda]/17に従って、コピーによってキャプチャされたエンティティを参照するid-expressionのみがラムダクロージャータイプのメンバーアクセスに変換されます。参照によってキャプチャされたエンティティを参照するid-expressionはそのまま残され、それでも囲みスコープで示されるのと同じエンティティを示します。

これはすぐに悪いように見えます:参照xの存続期間は終了しました。まあ、それは、その寿命以外の参照を参照する方法がほとんどない(以下を参照)ことがわかります(その宣言を見ることができます、その場合、それはスコープ内にあり、したがっておそらく使用してもOKです、またはそれはクラスですメンバ。この場合、メンバアクセス式が有効であるためには、クラス自体がその有効期間内でなければなりません。その結果、この規格には、ごく最近まで、その存続期間外の参照の使用に関する禁止事項がありませんでした。

ラムダの言葉遣いは、その存続期間外に参照を使用することにペナルティがないという事実を利用したため、参照によってキャプチャされたエンティティへのアクセスが何を意味するかについて明示的なルールを与える必要はありませんでした-それを使用するだけですエンティティ;参照の場合、名前はその初期化子を示します。そして、これがごく最近まで機能することが保証された方法です(C++ 11およびC++ 14を含む)。

ただし、その有効期間外の参照に言及できないのはquitetrueではありません。特に、独自の初期化子内から、参照よりも前のクラスメンバーの初期化子から、または名前空間スコープ変数であり、それより前に初期化された別のグローバルからアクセスする場合は、参照できます。 CWG issue 2012 はその見落としを修正するために導入されましたが、参照の参照によるラムダキャプチャの仕様を誤って破りました。 C++ 17が出荷される前に、この回帰を修正する必要があります。適切に優先順位が付けられていることを確認するために、National Bodyコメントを提出しました。

25
Richard Smith

TL; DR:問題のコードは標準によって保証されておらず、ラムダの合理的な実装があり、それを破ります。移植性がないと仮定して、代わりに

_std::function<void()> make_function(int& x)
{
    const auto px = &x;
    return [/* = */ px]{ std::cout << *px << std::endl; };
}
_

C++ 14からは、初期化されたキャプチャを使用してポインターを明示的に使用する必要がなくなります。これにより、スコープ内の変数を再利用する代わりに、ラムダの新しい参照変数が強制的に作成されます。

_std::function<void()> make_function(int& x)
{
    return [&x = x]{ std::cout << x << std::endl; };
}
_

一見すると、shouldは安全であるように見えますが、標準の文言には少し問題があります:

最小の囲みスコープがブロックスコープ(3.3.3)であるラムダ式は、ローカルラムダ式です。他のラムダ式は、ラムダイントロデューサーにキャプチャデフォルトまたはシンプルキャプチャを持たないものとします。 ローカルラムダ式の到達範囲は、最も内側の囲み関数とそのパラメーターを含む囲みスコープのセットです。 。

...

このような暗黙的にキャプチャされたすべてのエンティティは、ラムダ式の到達範囲内で宣言されます。

...

[注:エンティティが参照によって暗黙的または明示的にキャプチャされる場合、エンティティの有効期間が終了した後に対応するラムダ式の関数呼び出し演算子を呼び出すと、未定義の動作が発生する可能性があります。 —終了ノート]

発生することが予想されるのは、xが_make_function_内で使用されるとき、main()iを参照することです(参照が何をするのか)。エンティティiは参照によってキャプチャされます。そのエンティティはラムダ呼び出しの時点でまだ生きているので、すべてが良好です。

しかし! 「暗黙的にキャプチャされたエンティティ」は「ラムダ式の到達範囲内」でなければならず、main()iは到達範囲内にありません。 :(エンティティx自体が到達範囲外にある場合でも、パラメーターiが「到達範囲内で宣言されている」とカウントされない限り。

これは、C++の他の場所とは異なり、参照への参照が作成され、参照の存続期間に意味があるということです。

間違いなく、この規格が明確にしたいものです。

それまでの間、TL; DRセクションに示されているバリアントは、ポインターが値(ラムダオブジェクト自体に格納されている)によってキャプチャされ、ラムダの呼び出しを通して持続するオブジェクトへの有効なポインターであるため、間違いなく安全です。また、参照によるキャプチャは実際にはポインタを格納することになるため、実行時にペナルティはないはずです。


よく調べてみると、壊れる可能性もあります。 x86では、最終的なマシンコードで、EBP相対アドレス指定を使用してローカル変数と関数パラメーターの両方にアクセスすることに注意してください。パラメータは正のオフセットを持ち、ローカルは負です。 (他のアーキテクチャではレジスタ名が異なりますが、多くは同じように機能します。)とにかく、これはEBPの値のみをキャプチャすることで参照によるキャプチャを実装できることを意味します。次に、相対アドレス指定を使用して、ローカルおよびパラメーターを再度検索できます。実際、ラムダが定義された「スタックフレーム」をキャプチャすることを正確に行うラムダ実装(C++よりもずっと前にラムダがあった言語で)を聞いたことがあると思います。

これが意味するのは、_make_function_が返されてそのスタックフレームがなくなると、参照であるものも含め、ローカルANDパラメーターにアクセスするすべての機能がなくなることです。

また、この標準には、このアプローチを可能にする可能性が特に高い次のルールが含まれています。

追加の名前のない非静的データメンバーが、参照によってキャプチャされたエンティティのクロージャータイプで宣言されるかどうかは指定されていません。

結論:問題のコードは標準によって保証されておらず、ラムダの合理的な実装があり、それが壊れる可能性があります。移植性がないと仮定します。

27
Ben Voigt