私はC++でいくつかのラムダのメモリを操作していますが、そのサイズに少し困惑しています。
これが私のテストコードです。
#include <iostream>
#include <string>
int main()
{
auto f = [](){ return 17; };
std::cout << f() << std::endl;
std::cout << &f << std::endl;
std::cout << sizeof(f) << std::endl;
}
ここで実行できます: http://fiddle.jyt.io/github/b13f682d1237eb69ebdc60728bb52598
出力は次のとおりです。
17
0x7d90ba8f626f
1
これは、ラムダのサイズが1であることを示唆しています。
これはどのように可能ですか?
ラムダは、最低限、その実装へのポインタであるべきではありませんか?
問題のラムダには、実際にはno stateがあります。
調べる:
_struct lambda {
auto operator()() const { return 17; }
};
_
_lambda f;
_があった場合、それは空のクラスです。上記のlambda
はラムダに機能的に似ているだけでなく、(基本的に)ラムダの実装方法でもあります! (また、関数ポインター演算子への暗黙的なキャストが必要であり、名前lambda
はコンパイラー生成の疑似GUIDに置き換えられます)
C++では、オブジェクトはポインターではありません。それらは実際のものです。データを保存するために必要なスペースのみを使い果たします。オブジェクトへのポインタは、オブジェクトより大きくすることができます。
そのラムダを関数へのポインタと考えるかもしれませんが、そうではありません。 auto f = [](){ return 17; };
を別の関数またはラムダに再割り当てすることはできません!
_ auto f = [](){ return 17; };
f = [](){ return -42; };
_
上記は違法です。 f
に格納するスペースがありませんwhich関数が呼び出されます-その情報はf
の-typeに格納されます、f
!の値ではありません
これをした場合:
_int(*f)() = [](){ return 17; };
_
またはこれ:
_std::function<int()> f = [](){ return 17; };
_
ラムダを直接保存しなくなりました。どちらの場合も、f = [](){ return -42; }
は有効です。したがって、これらのケースでは、whichf
の値で呼び出す関数を格納しています。 sizeof(f)
は_1
_ではなく、sizeof(int(*)())
以上(基本的には、予想どおり、ポインターサイズ以上になります。_std::function
_は最小サイズです)少なくとも実際には関数ポインタと同じ大きさの標準によって暗示されています(特定のサイズまで「内部に」呼び出し可能オブジェクトを格納できる必要があります)。
int(*f)()
の場合、そのラムダを呼び出したかのように動作する関数への関数ポインターを格納しています。これは、ステートレスラムダ(空の_[]
_キャプチャリストを持つラムダ)でのみ機能します。
std::function<int()> f
の場合、型消去クラスstd::function<int()>
のインスタンスを作成します。このインスタンスは(この場合)配置newを使用して、サイズ1ラムダのコピーを内部バッファーに格納します(そして、より大きなラムダが(より多くの状態で)渡された場合、ヒープ割り当てを使用します)。
推測として、これらのようなものはおそらくあなたが起こっていると思うものです。ラムダは、その型がその署名で記述されているオブジェクトであること。 C++では、ラムダを作成することが決定されましたゼロコスト手動関数オブジェクトの実装を抽象化します。これにより、ラムダをstd
アルゴリズム(または類似のアルゴリズム)に渡し、アルゴリズムテンプレートをインスタンス化するときにコンパイラーにその内容を完全に表示させることができます。ラムダがstd::function<void(int)>
のような型を持っている場合、その内容は完全には表示されず、手作りの関数オブジェクトがより高速になる可能性があります。
C++の標準化の目標は、手作りのCコードよりもオーバーヘッドの少ない高レベルのプログラミングです。
f
が実際にステートレスであることを理解したので、頭の中に別の質問があるはずです。ラムダにはステートがありません。なぜ_0
_のサイズにならないのですか?
短い答えがあります。
C++のすべてのオブジェクトは、標準では最小サイズが1でなければならず、同じタイプの2つのオブジェクトが同じアドレスを持つことはできません。タイプT
の配列はsizeof(T)
離れた要素を配置するため、これらは接続されています。
現在、状態がないため、スペースを占有しない場合があります。これは「単独」の場合は発生しませんが、状況によっては発生する可能性があります。 _std::Tuple
_および同様のライブラリコードは、この事実を利用します。仕組みは次のとおりです。
ラムダはoperator()
オーバーロードされたクラスと同等であるため、ステートレスラムダ(_[]
_キャプチャリスト付き)はすべて空のクラスです。 _1
_のsizeof
があります。実際、それらから継承する場合(許可されています!)、それらはスペースを占有しません同じタイプのアドレス衝突を引き起こさない限り。 (これは空のベース最適化と呼ばれます)。
_template<class T>
struct toy:T {
toy(toy const&)=default;
toy(toy &&)=default;
toy(T const&t):T(t) {}
toy(T &&t):T(std::move(t)) {}
int state = 0;
};
template<class Lambda>
toy<Lambda> make_toy( Lambda const& l ) { return {l}; }
_
sizeof(make_toy( []{std::cout << "hello world!\n"; } ))
はsizeof(int)
です(評価されていないコンテキストでラムダを作成できないため、上記は違法です。名前付きauto toy = make_toy(blah);
を作成する必要がありますsizeof(blah)
を実行しますが、それは単なるノイズです)。 sizeof([]{std::cout << "hello world!\n"; })
は、まだ_1
_(同様の資格)です。
別のおもちゃタイプを作成する場合:
_template<class T>
struct toy2:T {
toy2(toy2 const&)=default;
toy2(T const&t):T(t), t2(t) {}
T t2;
};
template<class Lambda>
toy2<Lambda> make_toy2( Lambda const& l ) { return {l}; }
_
これには、ラムダの2つのコピーがあります。同じアドレスを共有できないため、sizeof(toy2(some_lambda))
は_2
_です!
ラムダは関数ポインターではありません。
ラムダはクラスのインスタンスです。あなたのコードは次のものとほぼ同等です:
_class f_lambda {
public:
auto operator() { return 17; }
};
f_lambda f;
std::cout << f() << std::endl;
std::cout << &f << std::endl;
std::cout << sizeof(f) << std::endl;
_
ラムダを表す内部クラスにはクラスメンバーがないため、そのsizeof()
は1です(適切に述べられている理由により0にはできません elsewhere )。
ラムダがいくつかの変数をキャプチャする場合、それらはクラスメンバーと同等であり、sizeof()
はそれに応じて示します。
コンパイラは、ラムダを多かれ少なかれ次の構造体型に変換します。
_struct _SomeInternalName {
int operator()() { return 17; }
};
int main()
{
_SomeInternalName f;
std::cout << f() << std::endl;
}
_
この構造体には非静的メンバーがないため、空の構造体と同じサイズ(_1
_)になります。
空でないキャプチャリストをラムダに追加するとすぐに変更されます。
_int i = 42;
auto f = [i]() { return i; };
_
に変換されます
_struct _SomeInternalName {
int i;
_SomeInternalName(int outer_i) : i(outer_i) {}
int operator()() { return i; }
};
int main()
{
int i = 42;
_SomeInternalName f(i);
std::cout << f() << std::endl;
}
_
生成された構造体は、キャプチャ用に非静的int
メンバーを格納する必要があるため、そのサイズはsizeof(int)
になります。より多くのものをキャプチャするにつれて、サイズは拡大し続けます。
(構造の類似性を一粒の塩で取ってください。ラムダが内部でどのように機能するかを推論する良い方法ですが、これはコンパイラが行うことの文字通りの翻訳ではありません)
ラムダは、最低限、その実装へのポインタではありませんか?
必ずしも。標準によれば、一意の名前のないクラスのサイズはimplementation-definedです。 [expr.prim.lambda]からの抜粋、C++ 14(エンファシス鉱山):
ラムダ式の型(クロージャオブジェクトの型でもあります)は、一意の名前のない非ユニオンクラス型(クロージャ型と呼ばれます)です。そのプロパティについては以下で説明します。
[...]
実装は以下で説明するものとは異なるクロージャー型を定義することができますプログラムの観察可能な動作を変更しない場合変更による以外:
-閉鎖タイプのサイズおよび/または配置、
—クロージャタイプが簡単にコピー可能かどうか(第9項)、
—クロージャタイプが標準レイアウトクラス(9項)であるか、または
—クロージャータイプがPODクラスであるかどうか(条項9)
あなたの場合-あなたが使用するコンパイラの場合-あなたは1のサイズを取得しますが、それが修正されているわけではありません。コンパイラの実装によって異なる場合があります。
http://en.cppreference.com/w/cpp/language/lambda から:
ラムダ式は、一意の名前のない非ユニオン非集約クラス型として知られる名前のないprvalue一時オブジェクトを構築します。これは、クロージャタイプとして知られ、 ADLの目的)ラムダ式を含む最小ブロックスコープ、クラススコープ、または名前空間スコープ。
lambda-expressionがcopyによって何かをキャプチャする場合(キャプチャ句[=]で暗黙的に、または文字&を含まないキャプチャで明示的になど) [a、b、c])、クロージャータイプには、名前のない非静的データメンバーが含まれ、不特定の順序で宣言され、すべてのエンティティのコピーを保持しますとても捕獲されました。
参照によってキャプチャされたエンティティの場合(デフォルトのキャプチャ[&]を使用する場合、または文字&を使用する場合(例:[&a、&b、&c]) 、クロージャー型で追加のデータメンバーが宣言されている場合、unspecified
から http://en.cppreference.com/w/cpp/language/sizeof
空のクラスタイプに適用すると、常に1を返します。