1. ホーム
  2. c++

C++11のラムダで参照を参照で捕捉する

2023-09-16 22:12:14

質問

これを考えてみましょう。

#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 が戻った後、ぶら下がった参照を持つことになるでしょうか。それとも、元々参照されていたオブジェクトがまだそこにある限り、キャプチャされた参照は動作することが保証されているのでしょうか?

ここで決定的な標準ベースの答えを探しています :) 実際には十分に機能する 今のところ ;)

どのように解決するのですか?

コードの動作は保証されています。

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


Ben Voigt の回答には事実誤認が含まれており、混乱を招いているため、いくつかの点を明らかにしたいと思います。

  1. スコープとは、C++ における静的で語彙的な概念であり、プログラム ソース コードの領域を記述し、非限定名前検索によって特定の名前を宣言と関連付けます。ライフタイムとは関係ありません。参照 [基本.スコープ.宣言的]/1 .
  2. ラムダの "reaching scope" 規則は、同様に、キャプチャがいつ許可されるかを決定する構文特性である。例えば

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

    n はここでスコープに入っていますが、ラムダの到達スコープには含まれていないので、捕捉することはできません。別の言い方をすれば、ラムダの到達スコープとは、どこまで到達して変数を捕捉できるかを示すもので、(ラムダ以外の)関数とそのパラメータまでは到達できるが、その外に到達して外に現れる宣言を捕捉できるわけではない。

ですから、スコープに到達するという概念は、この質問には関係ありません。キャプチャされるエンティティは make_function のパラメータ x で、これはラムダの到達範囲内にあります。


OK、ではこの問題についての標準の表現を見てみましょう。expr.prim.lambda]/17によると、唯一の id-expression のみがラムダクロージャ型のメンバーアクセスに変換されます。 id-式 はそのままにされ、包含するスコープで示されるのと同じ実体を示すようになります。

これはすぐに悪いことに思えます。 x の寿命は終わっているので、どうやってそれを参照するのでしょうか?その場合、その参照はスコープ内にあり、おそらく使用することができます。または、それはクラスのメンバーであり、メンバーアクセス式が有効であるためにクラス自体がその寿命内になければなりません。その結果、標準ではごく最近まで、寿命外の参照の使用は禁止されていませんでした。

ラムダの表現は、寿命の外で参照を使用してもペナルティがないという事実を利用し、参照によって捕捉された実体へのアクセスが何を意味するかについて、いかなる明示的なルールも与える必要がありませんでした - それはただ、その実体を使うことを意味します。そして、これはごく最近まで (C++11 と C++14 を含む) 動作が保証されていた方法です。

しかしながら、それは かなり 特に、それ自身のイニシャライザ内、参照より前のクラスメンバのイニシャライザ、またはそれが名前空間スコープ変数で、それ以前に初期化された別のグローバルからアクセスする場合は、参照できます。 CWG2012年問題 はこの見落としを修正するために導入されましたが、不注意にも、参照の参照によるラムダ捕捉の仕様を壊してしまいました。C++17 が出荷される前にこのリグレッションを修正すべきです。それが適切に優先されることを確認するために、私は National Body (国内団体) コメントを提出しました。