ラムダのサイズが1バイトなのはなぜですか?


89

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であることを示唆しています。

  • これはどのようにして可能ですか?

  • ラムダは、少なくとも、その実装へのポインタであってはなりませんか?


17
関数オブジェクトとして実装された(a structoperator()
george_ptr

14
また、空の構造体はサイズ0にできないため、結果は1になります。何かをキャプチャしてみて、サイズがどうなるかを確認してください。
Mohamad Elghawi、2016年

2
なぜラムダはポインタであるべきですか??? これは、呼び出し演算子を持つオブジェクトです。
Kerrek SB、2016

7
C ++のラムダはコンパイル時に存在し、呼び出しはコンパイル時またはリンク時にリンクされます(またはインライン化されます)。したがって、オブジェクト自体にランタイムポインタは必要ありません。@KerrekSBラムダを実装するほとんどの言語はC ++よりも動的であるため、ラムダに関数ポインタが含まれると予想するのは不自然な推測ではありません。
カイルストランド

2
@KerrekSB "重要なこと"-どのような意味ですか?理由(むしろ関数ポインタを含むより)閉鎖オブジェクトが空であることができるであるため、呼び出される関数をコンパイル/リンク時に知られています。これはOPが誤解しているようです。あなたのコメントがどのように明確にしているかはわかりません。
カイルストランド

回答:


107

問題のラムダには実際には状態がありません

診る:

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を格納する余地はありません。その情報は!の値ではなく、ので格納されfますf

これを行った場合:

int(*f)() = [](){ return 17; };

またはこれ:

std::function<int()> f = [](){ return 17; };

ラムダを直接保存しなくなりました。どちらの場合も、f = [](){ return -42; }は合法です。したがって、これらの場合、の値で呼び出す関数を格納ますf。そしてsizeof(f)、もはや1ではなく、sizeof(int(*)())それ以上です(基本的に、予想どおりポインタサイズ以上になります。 std::function標準によって暗示される最小サイズがあります(特定のサイズまで「自分自身の」呼び出し可能オブジェクトを格納できる必要があります)。少なくとも実際には関数ポインタと同じ大きさです)。

このint(*f)()場合、ラムダを呼び出した場合と同じように動作する関数への関数ポインタを格納します。これは、ステートレスラムダ([]キャプチャリストが空のラムダ)に対してのみ機能します。

このstd::function<int()> f場合、std::function<int()>(この場合は)新しい配置を使用してサイズ1のラムダのコピーを内部バッファーに格納する型消去クラスインスタンスを作成しています(より大きなラムダが渡された場合(より多くの状態) )、ヒープ割り当てを使用します)。

推測として、このようなものはおそらくあなたが考えていることです。ラムダは、そのシグネチャによって型が記述されているオブジェクトであること。C ++では、ラムダを手動の関数オブジェクト実装よりもコストをかけずに抽象化することが決定されました。これにより、ラムダをstdアルゴリズム(または類似のもの)に渡し、アルゴリズムテンプレートをインスタンス化するときにその内容をコンパイラーに完全に表示することができます。ラムダのタイプがのようなstd::function<void(int)>場合、その内容は完全に表示されず、手作りの関数オブジェクトの方が速い場合があります。

C ++標準化の目標は、手作りのCコードに対するオーバーヘッドがゼロの高水準プログラミングです。

あなたfは自分が実際にはステートレスであることを理解したので、頭の中に別の質問があるはずです。ラムダにはステートがありません。どうしてサイズがないの0


短い答えがあります。

C ++のすべてのオブジェクトは、標準では最小サイズ1である必要があり、同じタイプの2つのオブジェクトが同じアドレスを持つことはできません。タイプの配列はT要素がsizeof(T)離れて配置されるため、これらは接続されます。

現在、状態がないため、場所をとらない場合があります。これが「単独」の場合は発生しませんが、状況によっては発生する可能性があります。 std::tupleおよび同様のライブラリコードがこの事実を利用します。以下にその仕組みを示します。

ラムダはoperator()オーバーロードされたクラスと同等なので、ステートレスラムダ([]キャプチャリストを含む)はすべて空のクラスです。彼らは持っているsizeof1。実際、それらから継承する場合(許可されています!)、同じタイプのアドレスの衝突が発生しない限り、それらは領域を占有しません。(これは空の基本最適化と呼ばれます)。

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)(まあ、評価されていないコンテキストではラムダを作成できないため、上記は不正です。namedthen auto toy = make_toy(blah);do を作成する必要がありますが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


6
Nit:関数ポインタはvoid *よりも小さくできます。2つの歴史的な例:最初にwordofアドレスのマシンで、sizeof(void *)== sizeof(char *)> sizeof(struct *)== sizeof(int *)。(void *とchar *は、ワード内のオフセットを保持するためにいくつかの追加ビットを必要とします)2番目に、void * / int *がセグメント+オフセットであり、すべてのメモリをカバーできる8086メモリモデルですが、関数は単一の64Kセグメント内に適合しました(したがって、関数ポインタはわずか16ビットでした)。
Martin Bonnerがモニカをサポートする

1
@martin true。追加()されました。
Yakk-Adam Nevraumont 2016年

50

ラムダは関数ポインタではありません。

ラムダはクラスのインスタンスです。あなたのコードはほぼ以下と同等です:

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にすることはできません)。

ラムダがいくつかの変数をキャプチャする場合、それらはクラスメンバーと同等であり、sizeof()それに応じて表示されます。


3
sizeof()が0にならない理由を説明する「elsewhere」にリンクしていただけませんか。
user1717828 2016年

26

あなたのコンパイラは多かれ少なかれラムダを次の構造体型に変換します:

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)ます。より多くのものをキャプチャすると、サイズは拡大し続けます。

(構造体の類似点を一目で理解してください。ラムダが内部でどのように機能するかを推論する良い方法ですが、これはコンパイラーが行うことの文字通りの変換ではありません)


12

ラムダは、最低限、その実装へのポインタではないでしょうか?

必ずしも。標準によると、一意の名前のないクラスのサイズは実装定義です。[expr.prim.lambda]からの抜粋、C ++ 14(鉱山を強調):

ラムダ式の型(クロージャーオブジェクトの型でもあります)は、名前が付けられていない一意のununionクラス型(クロージャー型と呼ばれます)であり、そのプロパティについては以下で説明します。

[...]

実装では、以下の変更とは異なる方法でクロージャタイプを定義できます。ただし、これにより、変更以外のプログラム監視可能な動作が変更されない場合

-閉鎖型のサイズおよび/または位置合わせ

—クロージャタイプが簡単にコピーできるかどうか(条項9)、

—クロージャタイプが標準レイアウトクラス(条項9)であるか、または

—クロージャタイプがPODクラスかどうか(条項9)

あなたの場合-あなたが使うコンパイラーのために-あなたは1のサイズを取得します、それはそれが修正されたことを意味しません。コンパイラの実装によって異なる場合があります。


このビットが当てはまりますか?キャプチャグループのないラムダは、実際には「クロージャ」ではありません。(この規格では、空のキャプチャグループのラムダを「クロージャ」と呼んでいますか?)
カイルストランド

1
はい、そうです。これは、標準で「ラムダ式の評価の結果、一時的なprvalueが生成されます。この一時的なオブジェクトは、クロージャーオブジェクトと呼ばれます。と記述されています。キャプチャするかどうかにかかわらず、それはクロージャーオブジェクトです。
legends2k 2016年

私は反対票を投じなかったかもしれませんが、多分、反対投票者はこの回答が価値があるとは思わないでしょう。なぜなら、ランタイムへのポインターを含めずにラムダを実装することがなぜ(標準的な観点ではなく)可能であるかを説明していないからです。 call-operator関数。(質問の下でのKerrekSBとの私の議論を参照してください。)
カイルストランド

7

http://en.cppreference.com/w/cpp/language/lambdaから:

ラムダ式は、(ADLの目的で)含まれる最小のブロックスコープ、クラススコープ、または名前空間スコープで宣言される、クロージャータイプ呼ばれる、一意の無名非ユニオン非集合クラスタイプの無名prvalue一時オブジェクトを作成しますラムダ式。

lambda-expressionがコピーによって何かをキャプチャする場合(キャプチャ句[=]で暗黙的に、または文字&を含まないキャプチャで明示的に[a、b、c]など)、クロージャタイプには名前のない非静的データが含まれます不特定の順序で宣言された、そのようにキャプチャされたすべてのエンティティのコピーを保持するメンバー

参照によってキャプチャーされるエンティティー(デフォルトのキャプチャー[&]または文字&を使用する場合、例:[&a、&b、&c])、追加のデータメンバーがクロージャータイプで宣言されているかどうか指定されていません

http://en.cppreference.com/w/cpp/language/sizeofから

空のクラスタイプに適用すると、常に1を返します。

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