21

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で安全に返せるクロージャは、それらを使用する上で最も重要になると思いますが、これをよりエレガントに実現する方法を知っている人はいますか?

4

2 に答える 2

24

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 には、複数のステートメントでも戻り値の型を推定できる拡張機能があると思いますが、少なくとも警告が表示されるはずです。)

于 2012-05-20T01:33:08.757 に答える