2015-01-05 11 views
8

ローカル変数を2つのラムダに渡しています。私はこれらのラムダを関数スコープの外に呼びます。これはundefinedですか?ラムダと参照ローカル変数によるキャプチャ:スコープの後のアクセス

std::pair<std::function<int()>, std::function<int()>> addSome() { 
    int a = 0, b = 0; 
    return std::make_pair([&a,&b] { 
     ++a; ++b; 
     return a+b; 
     }, [&a, &b] { 
      return a; 
     }); 
} 

int main() { 
    auto f = addSome(); 
    std::cout << f.first() << " " << f.second(); 
    return 0; 
} 

ただし、そうでない場合は、1つのλの変化は他のλに反映されません。

ラムダの文脈で参照渡しを誤解していますか?

私は変数に書いていますし、それが出力

2 0ていないランタイムエラーで正常に動作しているようです。それが動作すれば私は出力2 1を期待するでしょう。

答えて

17

はい、これは未定義の動作を引き起こします。ラムダは、範囲外になったスタック割り当てオブジェクトを参照します。 (私はそれを理解して技術的には、動作はラムダアクセスaおよび/またはbまでに定義されています。あなたが返さラムダを呼び出すことはありません場合は、何のUBはありません。)これは未定義の動作は、それが未定義だと同じよう

ですスタックに割り当てられたローカルへの参照を返す動作、そしてこの場合はラムダによって少し難読化されていることを除いて、ローカルがスコープ外になった後でその参照を使用します。

さらにlambdaが呼び出される順序は指定されていないことに注意してください。の前にはコンパイラはf.second()を自由に呼び出して、両方が同じ完全式の一部であるためです。したがって、破棄されたオブジェクトへの参照を使用することによって引き起こされる未定義の動作を修正したとしても、2 02 1です。このプログラムからの有効な出力はコンパイラがラムダを実行することを決定する順序によって異なります。コンパイラがの何ものも行うことができないので、は未定義の動作ではないことに注意してください。を行うには、の順番を決めるのにいくつか自由があります。

(あなたmain()機能で<<は、カスタムoperator<<機能を起動して、関数の引数が評価される順序が指定されていないことに留意してください。コンパイラは、同じフル内の関数の引数のすべてを評価し、コードを放出するのは自由です関数のすべての引数を評価してから関数を呼び出さなければならないという制約があります)。

最初の問題を解決するには、std::shared_ptrを使用して参照カウントオブジェクトを作成します。この共用ポインタを値で取り込みます。ラムダは、それら(およびそのコピー)が存在する限り、ポインティング先オブジェクトを生きたままにします。このヒープ割り当てオブジェクトは、共有状態abを格納する場所です。

2番目の問題を解決するには、各ラムダを別のステートメントで評価します。ここで

は、あなたのコードは、固定され、未定義の動作に書き換え、そしてf.first()f.second()前に呼び出されることが保証されています

std::pair<std::function<int()>, std::function<int()>> addSome() { 
    // We store the "a" and "b" ints instead in a shared_ptr containing a pair. 
    auto numbers = std::make_shared<std::pair<int, int>>(0, 0); 

    // a becomes numbers->first 
    // b becomes numbers->second 

    // And we capture the shared_ptr by value. 
    return std::make_pair(
     [numbers] { 
      ++numbers->first; 
      ++numbers->second; 
      return numbers->first + numbers->second; 
     }, 
     [numbers] { 
      return numbers->first; 
     } 
    ); 
} 

int main() { 
    auto f = addSome(); 
    // We break apart the output into two statements to guarantee that f.first() 
    // is evaluated prior to f.second(). 
    std::cout << f.first(); 
    std::cout << " " << f.second(); 
    return 0; 
} 

See it runを。)

7

残念ながら、C++のlambdasは参考として取り込むことができますが、 "upwards funarg problem"は解決しません。

このようにするには、キャプチャされたローカルを「セル」に割り当てる必要があります。また、ガベージコレクションや解放の参照カウントが必要です。 C++はそれをやっていません。残念なことに、これはC++ラムダを、Lisp、Python、Javascriptのような他の言語よりもあまり有用でなく危険なものにします。

具体的には、後でメンテナンス時にランダムなsegfaultsのレシピであるため、ローカルスコープで生き残るラムダオブジェクトに対して、参照による暗黙のキャプチャ(つまり、[&](…){…}形式の使用)を避けるべきです。

キャプチャする内容とキャプチャされた参照の有効期間と方法については、常に慎重に計画します。

(もちろん、それはあなたがやっているすべては、単に関数の外という名前のコンパレータ機能を定義することなくstd::sortのようなアルゴリズムにコードを渡すために同じスコープにラムダを使用している場合は、参照によってすべてを捕獲しても安全です。)

時には、ヒープ割り当て状態に a shared_ptrをキャプチャする方法があります。これは、基本的にはPythonが自動的に行う処理を手作業で実装しています(ただし、メモリリークを防ぐための参照サイクルに注意してください:Pythonにはガベージコレクタがあり、C++ではそうではありません)。

+2

'[b]閉鎖およびそのコピーの寿命が封入に限られている場合、' '} {結構です'それ以外の場合は、危険で危険です。そのような制限された生涯閉鎖が一般的であるので、一般的な規則としてそれを避けることは過度のようですか? – Yakk

+1

@ Yakk:私はアルゴリズムでコードを埋め込む貧弱な方法ではなく、クロージャーとしてのラムダの使用を考えていました。私はそれを説明するために編集します。 – 6502

+0

funarg問題を指摘し、他の言語と比較するための最良の答え。 – Maggyero

0

範囲外になるとき。これにより、あなたの地元の人のコピーが作成されます。あなたは、参照することにより使用する、スコープに優れている

void func(void) 
{ 
    auto mylocal = getFromSomeWhere(); 
    doSearch([=] { 
     mylocal->foudnSomthing(); 
    }); 
} 

MyType func(void) 
{ 
    auto mylocal = getFromSomeWhere(); 
    return ([&] { 
     return mylocal->doOperation(); 
    }) 
} 
関連する問題