C ++ 11ラムダ実装とメモリモデル


92

C ++ 11クロージャーについて正しく考える方法とstd::function、それらがどのように実装され、メモリーがどのように処理されるかという点について、いくつかの情報が欲しいのですが。

私は時期尚早の最適化を信じていませんが、私は新しいコードを書くときに自分の選択がパフォーマンスに与える影響を慎重に検討する習慣があります。また、マイクロコントローラーやオーディオシステムなど、非決定的なメモリ割り当て/割り当て解除の一時停止を回避するために、かなりの量のリアルタイムプログラミングを行っています。

したがって、C ++ラムダをいつ使用するか、または使用しないかについての理解を深めたいと思います。

私の現在の理解では、キャプチャされたクロージャのないラムダは、Cコールバックとまったく同じです。ただし、環境が値または参照によってキャプチャされると、匿名オブジェクトがスタックに作成されます。関数から値のクロージャを返す必要がある場合は、それをでラップしstd::functionます。この場合、クロージャメモリはどうなりますか?スタックからヒープにコピーされますか?が解放されるたびに解放されますか?std::functionつまり、aのように参照カウントされstd::shared_ptrますか?

リアルタイムシステムでラムダ関数のチェーンをセットアップし、Bを継続引数としてAに渡して、処理パイプラインA->Bが作成されると思います。この場合、AクロージャーとBクロージャーは一度割り当てられます。これらがスタックとヒープのどちらに割り当てられるかはわかりませんが。ただし、一般的に、これはリアルタイムシステムで安全に使用できます。一方、Bがラムダ関数Cを作成してそれが返す場合、Cのメモリは繰り返し割り当てと割り当て解除が行われ、リアルタイムの使用には受け入れられません。

疑似コードでは、DSPループはリアルタイムで安全になると思います。ブロックAを処理してからBを処理したいのですが、Aは引数を呼び出します。これらの関数は両方ともstd::functionオブジェクトを返すのでfstd::functionオブジェクトは、環境がヒープに格納されます。

auto f = A(B);  // A returns a function which calls B
                // Memory for the function returned by A is on the heap?
                // Note that A and B may maintain a state
                // via mutable value-closure!
for (t=0; t<1000; t++) {
    y = f(t)
}

そして、私がリアルタイムコードで使うのが悪いと思うもの:

for (t=0; t<1000; t++) {
    y = A(B)(t);
}

そして、スタックメモリがクロージャに使用されている可能性が高いと私が考える1つ:

freq = 220;
A = 2;
for (t=0; t<1000; t++) {
    y = [=](int t){ return sin(t*freq)*A; }
}

後者の場合、クロージャーはループの反復ごとに作成されますが、前の例とは異なり、関数呼び出しのように安価なので、ヒープの割り当ては行われません。さらに、コンパイラーがクロージャーを「持ち上げ」、インライン最適化を行うことができるのかと思います。

これは正しいです?ありがとうございました。


4
ラムダ式を使用してもオーバーヘッドはありません。もう1つの選択肢は、そのような関数オブジェクトを自分で作成することです。これはまったく同じです。ところで、インラインの質問では、コンパイラーは必要なすべての情報を持っているので、への呼び出しをインライン化することができoperator()ます。実行する「リフティング」はありません。ラムダは特別なものではありません。これらはローカル関数オブジェクトの省略形にすぎません。
Xeo 2012

これは、std::functionその状態をヒープに保存するかどうかに関する問題のようで、ラムダとは関係ありません。そうですか?
Mooing Duck 2012

8
ただ、誤解した場合に、それをスペルアウトするには:ラムダ式がありませんstd::function
Xeo 2012

1
補足コメント:関数からラムダを返すときは注意してください。参照によってキャプチャされたローカル変数は、ラムダを作成した関数を離れると無効になります。
ジョルジオ

2
@Steve C ++ 14以降では、auto戻り値の型を持つ関数からラムダを返すことができます。
Oktalist 2018

回答:


100

私の現在の理解では、キャプチャされたクロージャのないラムダは、Cコールバックとまったく同じです。ただし、環境が値または参照によってキャプチャされると、匿名オブジェクトがスタックに作成されます。

番号; これは常に、スタック上に作成された不明なタイプのC ++オブジェクトです。キャプチャレスラムダは関数ポインターに変換できます(ただし、C呼び出し規約に適しているかどうかは実装に依存します)が、それ関数ポインターであると限りません。

関数から値のクロージャを返す必要がある場合、それをstd :: functionでラップします。この場合、クロージャメモリはどうなりますか?

C ++ 11ではラムダは特別なものではありません。他のオブジェクトと同様のオブジェクトです。ラムダ式の結果は一時的になり、これを使用してスタック上の変数を初期化できます。

auto lamb = []() {return 5;};

lambスタックオブジェクトです。コンストラクタとデストラクタがあります。そして、そのためのすべてのC ++ルールに従います。のタイプにlambは、キャプチャされる値/参照が含まれます。他のタイプの他のオブジェクトメンバーと同じように、それらはそのオブジェクトのメンバーになります。

あなたはそれを与えることができますstd::function

auto func_lamb = std::function<int()>(lamb);

この場合、の値のコピーを取得しますlamb。場合lamb値によって捕獲ものを持っていた、それらの値の2つのコピーが存在することになります。に1つ、lambに1つfunc_lamb

現在のスコープが終了すると、スタック変数のクリーンアップのルールに従って、func_lambが破棄され、その後にが続きlambます。

同じように、ヒープに1つを簡単に割り当てることができます。

auto func_lamb_ptr = new std::function<int()>(lamb);

aのコンテンツ用のメモリの正確な場所std::functionは実装に依存しますが、が使用するタイプ消去は、std::function通常、少なくとも1つのメモリ割り当てを必要とします。これが、std::functionコンストラクタがアロケータを取ることができる理由です。

std :: functionが解放されるたびに解放されますか、つまり、std :: shared_ptrのように参照カウントされますか?

std::functionその内容のコピーを保存します。ほぼすべての標準ライブラリのC ++タイプと同様に、値のセマンティクスをfunction使用します。したがって、コピー可能です。コピーすると、新しいfunctionオブジェクトは完全に分離されます。また、移動可能であるため、内部割り当てを適切に転送して、追加の割り当てやコピーを行う必要はありません。

したがって、参照カウントの必要はありません。

「メモリの割り当て」が「リアルタイムコードでの使用に適さない」と同等であると想定すると、それ以外のすべての記述は正しいです。


1
素晴らしい説明、ありがとうございます。したがって、の作成はstd::function、メモリが割り当てられ、コピーされるポイントです。最初ににコピーしないと、(スタックに割り当てられているため)クロージャーを返す方法はないようstd::functionです。
スティーブ

3
@Steve:はい。ラムダをスコープから抜け出すには、ある種のコンテナでラップする必要があります。
Nicol Bolas

関数のコード全体がコピーされていますか、それとも元の関数はコンパイル時に割り当てられ、クローズされた値を渡しましたか?
ラマゲドン

標準が多かれ少なかれ間接的に義務付けていることを追加したい(§20.8.11.2.1 [func.wrap.func.con]¶5)ラムダが何もキャプチャしない場合、std::function動的メモリなしでオブジェクトに格納できること割り当てが進行中です。
5gon12eder 2015年

2
@ヤク:「大」をどのように定義しますか?状態の2つのポインタを持つオブジェクトは「大きい」ですか?3か4はどうですか?また、オブジェクトのサイズだけが問題ではありません。オブジェクトがスロー移動できない場合は、noexcept moveコンストラクターがあるため、オブジェクトを割り当てに格納する必要ありますfunction。「一般的に必要」と言っているのは、「常に必要」と言っているのではなく、割り当てが行われない状況があるということです。
Nicol Bolas

0

C ++ラムダが過負荷でだけ糖衣構文の周りの(匿名)のFunctorクラスであるoperator()std::functionし、呼び出し可能オブジェクト(すなわちファンクタ、ラムダ、C-機能、...)単なるラッパーである値でコピーを現在から「固体ラムダオブジェクト」スタックスコープ- ヒープに

実際のコンストラクター/リロカトンの数をテストするために、テストを行いました(shared_ptrへの別のレベルのラップを使用していますが、そうではありません)。自分で見て:

#include <memory>
#include <string>
#include <iostream>

class Functor {
    std::string greeting;
public:

    Functor(const Functor &rhs) {
        this->greeting = rhs.greeting;
        std::cout << "Copy-Ctor \n";
    }
    Functor(std::string _greeting="Hello!"): greeting { _greeting } {
        std::cout << "Ctor \n";
    }

    Functor & operator=(const Functor & rhs) {
        greeting = rhs.greeting;
        std::cout << "Copy-assigned\n";
        return *this;
    }

    virtual ~Functor() {
        std::cout << "Dtor\n";
    }

    void operator()()
    {
        std::cout << "hey" << "\n";
    }
};

auto getFpp() {
    std::shared_ptr<std::function<void()>> fp = std::make_shared<std::function<void()>>(Functor{}
    );
    (*fp)();
    return fp;
}

int main() {
    auto f = getFpp();
    (*f)();
}

それはこの出力をします:

Ctor 
Copy-Ctor 
Copy-Ctor 
Dtor
Dtor
hey
hey
Dtor

スタックに割り当てられたラムダオブジェクトに対して、まったく同じ一連のctor / dtorが呼び出されます。(現在は、スタック割り当てのためにCtorを呼び出し、std :: functionでそれを構築するためにCopy-ctor(+ heap alloc)を呼び出し、shared_ptrヒープ割り当て+関数の構築のためにもう1つ呼び出します)

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.