33

c++11 では、ラムダを渡すのは非常に簡単です。

func( []( int arg ) {
  // code
} ) ;

しかし、このような関数にラムダを渡すコストはいくらですか? func がラムダを他の関数に渡すとどうなるでしょうか?

void func( function< void (int arg) > f ) {
  doSomethingElse( f ) ;
}

ラムダの受け渡しは高価ですか? functionオブジェクトには0 を割り当てることができるため、

function< void (int arg) > f = 0 ; // 0 means "not init" 

関数オブジェクトはポインタのように振る舞うと思います。しかし、を使用newしないと、値型またはクラスのようになる可能性がありstruct、デフォルトでスタック割り当てとメンバーごとのコピーになります。

関数オブジェクトを「値渡し」で渡す場合、C++11 の「コード本体」とキャプチャされた変数のグループはどのように渡されますか? コード本体の余分なコピーが多い?コピーが作成されないように、function渡された各オブジェクトにマークを付ける必要がありますか?const&

void func( const function< void (int arg) >& f ) {
}

それとも、関数オブジェクトは通常の C++ 構造体とは異なる方法で渡すのでしょうか?

4

3 に答える 3

34

免責事項:私の答えは現実に比べていくらか単純化されています(詳細は脇に置きます)が、全体像はここにあります. また、標準では、ラムダまたはstd::function内部で実装する必要がある方法を完全に指定していません (実装にはある程度の自由があります)。そのため、実装の詳細に関する他の議論と同様に、コンパイラはこの方法で正確に実行する場合と実行しない場合があります。

しかし、繰り返しになりますが、これは VTables と非常によく似た主題です。標準ではあまり義務付けられていませんが、賢明なコンパイラはこの方法で行う可能性が非常に高いため、少し掘り下げる価値があると思います。:)


ラムダス

ラムダを実装する最も簡単な方法は、名前のないものstructです。

auto lambda = [](Args...) -> Return { /*...*/ };

// roughly equivalent to:
struct {
    Return operator ()(Args...) { /*...*/ }
}
lambda; // instance of the unnamed struct

他のクラスと同様に、そのインスタンスを渡すときに、コードをコピーする必要はなく、実際のデータだけをコピーする必要があります (ここでは、まったくコピーしません)。


値によってキャプチャされたオブジェクトは、次のファイルにコピーされますstruct:

Value v;
auto lambda = [=](Args...) -> Return { /*... use v, captured by value...*/ };

// roughly equivalent to:
struct Temporary { // note: we can't make it an unnamed struct any more since we need
                   // a constructor, but that's just a syntax quirk

    const Value v; // note: capture by value is const by default unless the lambda is mutable
    Temporary(Value v_) : v(v_) {}
    Return operator ()(Args...) { /*... use v, captured by value...*/ }
}
lambda(v); // instance of the struct

v繰り返しますが、それを渡すということは、コード自体ではなく、データ ( ) を渡すことを意味するだけです。


同様に、参照によってキャプチャされたオブジェクトは、次のように参照されますstruct

Value v;
auto lambda = [&](Args...) -> Return { /*... use v, captured by reference...*/ };

// roughly equivalent to:
struct Temporary {
    Value& v; // note: capture by reference is non-const
    Temporary(Value& v_) : v(v_) {}
    Return operator ()(Args...) { /*... use v, captured by reference...*/ }
}
lambda(v); // instance of the struct

ラムダ自体に関しては、これでほとんどすべてです (省略したいくつかの実装の詳細を除いて、それがどのように機能するかを理解することには関係ありません)。


std::function

std::functionは、あらゆる種類のファンクター (ラムダ、スタンドアロン/静的/メンバー関数、私が示したようなファンクター クラスなど) の汎用ラッパーです。

std::functionこれらすべてのケースをサポートする必要があるため、の内部はかなり複雑です。ファンクターの正確なタイプに応じて、これには少なくとも次のデータが必要です (実装の詳細を提供または取得します)。

  • スタンドアロン/静的関数へのポインター。

または、

  • ファンクターのコピーへのポインター[以下の注を参照] (あなたが正しく指摘したように、任意のタイプのファンクターを許可するために動的に割り当てられます)。
  • 呼び出されるメンバー関数へのポインター。
  • ファンクターとそれ自体の両方をコピーできるアロケーターへのポインター (任意のタイプのファンクターを使用できるため、ポインターからファンクターへのポインターを使用する必要がvoid*あり、そのようなメカニズムが必要です。おそらく、ポリモーフィズム aka. base を使用します)。クラス + 仮想メソッド、派生クラスはtemplate<class Functor> function(Functor)コンストラクターでローカルに生成されます)。

どの種類のファンクターを格納する必要があるかを事前に知らないため (これは、std::function再割り当てできるという事実から明らかです)、考えられるすべてのケースに対処し、実行時に決定を下す必要があります。

注:標準がどこでそれを義務付けているかはわかりませんが、これは間違いなく新しいコピーであり、基礎となるファンクターは共有されていません:

int v = 0;
std::function<void()> f = [=]() mutable { std::cout << v++ << std::endl; };
std::function<void()> g = f;

f(); // 0
f(); // 1
g(); // 0
g(); // 1

したがって、 a を渡すと、std::function少なくともこれらの 4 つのポインター (実際、GCC 4.7 では 64 ビットsizeof(std::function<void()>は 32、つまり 4 つの 64 ビット ポインター) と、オプションでファンクターの動的に割り当てられたコピー (既に述べたように、含まれるもののみ) が含まれます。キャプチャされたオブジェクト、コードをコピーしないでください)。


質問への回答

このような関数にラムダを渡すコストはいくらですか? [質問の文脈:値による]

ご覧のとおり、主にファンクター (手作りのstructファンクターまたはラムダ) とそれに含まれる変数に依存します。ファンクターを直接値渡しする場合と比較した場合のオーバーヘッドstructはごくわずかですが、struct参照渡しの場合よりもはるかに高くなります。

const&コピーが作成されないように、渡された各関数オブジェクトをマークする必要がありますか?

これは一般的な方法で答えるのが非常に難しいと思います。移動できるように、参照渡しconst、値渡し、右辺値参照渡しが必要な場合があります。それは本当にコードのセマンティクスに依存します。

どちらを選択するかに関するルールは、IMO とはまったく別のトピックですが、他のオブジェクトと同じであることを覚えておいてください。

とにかく、これで情報に基づいた決定を下すためのすべての鍵が手に入りました (これも、コードとそのセマンティクスによって異なります)。

于 2013-06-04T15:45:36.367 に答える
4

C++11 ラムダ実装とメモリ モデルも参照してください

ラムダ式はまさにそれ、つまり式です。コンパイルされると、実行時にクロージャ オブジェクトになります。

5.1.2 ラムダ式 [expr.prim.lambda]

ラムダ式の評価は、一時的な prvalue (12.2) になります。この一時的なものはクロージャー オブジェクトと呼ばれます。

オブジェクト自体は実装定義であり、コンパイラごとに異なる場合があります。

これは、clang でのラムダの元の実装です https://github.com/faisalv/clang-glambda

于 2013-06-04T15:50:22.073 に答える
1

ラムダを単純な関数として作成できる場合 (つまり、何もキャプチャしない場合)、まったく同じ方法で作成されます。特に、標準では、同じシグネチャを持つ古いスタイルの関数へのポインタと互換性がある必要があります。[編集: 正確ではありません。コメントの議論を参照してください]

残りは実装次第ですが、先の心配はありません。最も単純な実装では、情報を運ぶだけです。キャプチャで求めたのとまったく同じです。そのため、手動でクラスを作成した場合と同じ効果が得られます。または、いくつかの std::bind バリアントを使用します。

于 2013-06-04T15:43:53.673 に答える