2012-05-20 3 views
20

C++ 11ラムダは素晴らしいです!C++ 11のラムダ構文では、ヒープ割り当てのクロージャ?

しかし、変更可能なデータを安全に処理する方法の1つが欠落しています。

以下は、最初のカウントの後に悪いカウントを与える:

#include <cstdio> 
#include <functional> 
#include <memory> 

std::function<int(void)> f1() 
{ 
    int k = 121; 
    return std::function<int(void)>([&]{return k++;}); 
} 

int main() 
{ 
    int j = 50; 
    auto g = f1(); 
    printf("%d\n", g()); 
    printf("%d\n", g()); 
    printf("%d\n", g()); 
    printf("%d\n", g()); 
} 

を与え、

$ g++-4.5 -std=c++0x -o test test.cpp && ./test 
121 
8365280 
8365280 
8365280 

理由はf1()戻った後、kがスコープの外に、まだスタックにあるということです。したがって、最初にg()が実行されたのはkですが、それ以降はスタックが破損し、kの値が失われます。だから、

、私はC++ 11で安全に通いクロージャを作ることができた唯一の方法は、ヒープ上に明示的に閉じられた変数を割り当てることである。ここでは

std::function<int(void)> f2() 
{ 
    int k = 121; 
    std::shared_ptr<int> o = std::shared_ptr<int>(new int(k)); 
    return std::function<int(void)>([=]{return (*o)++;}); 
} 

int main() 
{ 
    int j = 50; 
auto g = f2(); 
    printf("%d\n", g()); 
    printf("%d\n", g()); 
    printf("%d\n", g()); 
    printf("%d\n", g()); 
} 

[=]は、共有ポインタを確実にするために使用されますがコピーされたは参照されないため、メモリの処理は正しく行われます。kのヒープ割り当てコピーは、生成された関数gが有効範囲外になると解放されます。希望通りの結果が

$ g++-4.5 -std=c++0x -o test test.cpp && ./test 
121 
122 
123 
124 

、あるそれはそれらを参照解除することにより、変数を参照するためにはかなり醜いですが、代わりに参照を使用することが可能です:

std::function<int(void)> f3() 
{ 
    int k = 121; 
    std::shared_ptr<int> o = std::shared_ptr<int>(new int(k)); 
    int &p = *o; 
    return std::function<int(void)>([&]{return p++;}); 
} 

実は、これは奇妙なことに、私に

を与えます
$ g++-4.5 -std=c++0x -o test test.cpp && ./test 
0 
1 
2 
3 

理由は何ですか?共有ポインタの参照を取得するのは丁寧ではないかもしれません。追跡された参照ではないので、今考えてみましょう。私は、ラムダの内側への参照を移動すると自動的に経由して安全に通いクロージャを作るための方法があった場合、それはいいだろういずれにしてもクラッシュ、

std::function<int(void)> f4() 
{ 
    int k = 121; 
std::shared_ptr<int> o = std::shared_ptr<int>(new int(k)); 
    return std::function<int(void)>([&]{int &p = *o; return p++;}); 
} 

寄付、

g++-4.5 -std=c++0x -o test test.cpp && ./test 
156565552 
/bin/bash: line 1: 25219 Segmentation fault  ./test 

の原因となることがわかりましたヒープ割り当て。たとえば、[=][&]の代わりに、変数がヒープ割り当てされ、共有ポインタへの参照を介して参照される必要があることが示されている場合。私が最初に考えたのは、std::functionについて学んだときに、クロージャをカプセル化するオブジェクトを作成していたため、クロージャ環境のストレージを提供することができましたが、私の実験ではこれは役に立たないようです。

私はC++ 11で安全にリターン可能なクロージャが重要な役割を果たしていると考えています。

+2

Valgrindの内部では、このようなテストを実行する必要があります。これは、割り当て解除されたメモリにアクセスする際に正しく動作することを確認しているからです。 – Potatoswatter

+0

static int k = 121; – adnako

答えて

22

f1あなたが言う理由で未定義の動作が発生しています。ラムダにはローカル変数への参照が含まれており、関数が返った後も参照はもはや有効ではありません。

int k = 121; 
return std::function<int(void)>([=]() mutable {return k++;}); 

あなたが異なるコピーするので、しかし、このラムダの使用について注意する必要があります:あなたはヒープ上に割り当てる必要はありませんこれを回避するには、単にそのキャプチャ値は変更可能です宣言する必要がキャプチャされた変数のコピーを変更します。アルゴリズムはしばしば、ファンクタのコピーを使用することは元のものを使用することと同等であると考えている。私は、ステートフルな関数オブジェクト、std :: for_eachのための余裕を実際に作るアルゴリズムは1つしかないと思います。ここで、使用される関数オブジェクトの別のコピーが返されます。 f3何で


共有ポインタのコピーを維持しているので、メモリが解放されて、それは未定義の動作を与えるアクセスします。あなたは明示的に共有ポインタを値で捕捉することでこれを修正することができ、依然として指し示されたintを参照で捕捉することができます。

std::shared_ptr<int> o = std::shared_ptr<int>(new int(k)); 
int &p = *o; 
return std::function<int(void)>([&p,o]{return p++;}); 

f4あなたは再びローカル変数、oへの参照をキャプチャしているので、再び未定義の動作です。あなたは単に値でキャプチャする必要がありますが、あなたが望む構文を得るためにラムダの中にあなたのint &pを作成する必要があります。

std::shared_ptr<int> o = std::shared_ptr<int>(new int(k)); 
return std::function<int(void)>([o]() -> int {int &p = *o; return p++;}); 

2番目のステートメントC++ 11を追加すると、戻り値の型を省略することができなくなります。 (clangと私はgccが複数のステートメントでも戻り値のタイプ控除を許可する拡張を持っていると仮定しますが、少なくとも警告を受け取るべきです)。

+0

旧式のファンクタよりラムダで状態にアクセスするのは難しいことに注意してください。あなたができる唯一のことは、それをもう一度呼び出すことです。しかし、ステートフルなラムダは補助的な結果を返さなくてもまだ役立ちます。 – Potatoswatter

+0

"*ステートフルなラムダを実際に使用するアルゴリズムは1つしかないと思います。std :: accumulateは、使用したラムダを返すので、何らかの変更が発生した場合でもアクセスできます。*"これはあなたのためのものです。思う。 – ildjarn

+0

@ildjarn修正しました、ありがとうございます。 – bames53

1

ここに私のテストコードがあります。再帰関数を使用して、ラムダパラメータのアドレスと他のスタックベースの変数を比較します。

#include <stdio.h> 
#include <functional> 

void fun2(std::function<void()> callback) { 
    (callback)(); 
} 

void fun1(int n) { 
    if(n <= 0) return; 
    printf("stack address = %p, ", &n); 

    fun2([n]() { 
     printf("capture address = %p\n", &n); 
     fun1(n - 1); 
    }); 
} 

int main() { 
    fun1(200); 
    return 0; 
} 

はにMinGW64でコードをコンパイルし、Win7の上で動作する、それが

stack address = 000000000022F1E0, capture address = 00000000002F6D20 
stack address = 000000000022F0C0, capture address = 00000000002F6D40 
stack address = 000000000022EFA0, capture address = 00000000002F6D60 
stack address = 000000000022EE80, capture address = 00000000002F6D80 
stack address = 000000000022ED60, capture address = 00000000002F6DA0 
stack address = 000000000022EC40, capture address = 00000000002F6DC0 
stack address = 000000000022EB20, capture address = 00000000007A7810 
stack address = 000000000022EA00, capture address = 00000000007A7820 
stack address = 000000000022E8E0, capture address = 00000000007A7830 
stack address = 000000000022E7C0, capture address = 00000000007A7840 

出力は、キャプチャパラメータはスタック領域上に配置されていないことは明らかです とキャプチャパラメータのアドレスは、です継続的ではない

だから私はいくつかのコンパイラは
キャプチャラムダパラメータに動的メモリ割り当てを使用する可能性があると考えています。

+1

Lambdaオブジェクトは、通常の関数オブジェクトを作成するための単なる構文的な砂糖です。キャプチャされた変数は、関数オブジェクトのメンバになります。動的なメモリ割り当ては、特にそれを行うコードを記述しないかぎり使用されません。 – bames53

+0

@ bames53この記事によると:http://www.drdobbs.com/cpp/efficient-use-of-lambda-expressions-and/232500059?pgno=2、std :: function実装は実際に動的割り当てを行うことができます。 – marcinj

+0

@brightstar私はラムダオブジェクトのコンパイラの実装について話しています。上記の答えは、 'std :: function <>'実装の動的割り当てをコンパイラのlambdaオブジェクトの実装に誤っています。 – bames53

関連する問題