1. ホーム
  2. c++

[解決済み] なぜラムダのサイズは1バイトなのですか?

2023-02-14 02:30:07

質問

私は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;
}

ouptutは。

17
0x7d90ba8f626f
1

これは、私のラムダのサイズが1であることを示唆しています。

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

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

どのように解決するのですか?

問題のラムダは、実際には 状態なし .

調べる。

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 は標準によって暗示された最小サイズ(それらはあるサイズまで "自身の内部" callables を格納できる必要があります)を持ち、それは実際には少なくとも関数ポインタと同じ大きさです)。

では int(*f)() の場合、そのラムダを呼び出したかのように振る舞う関数への関数ポインタを保存していることになります。 これはステートレスなラムダ (空の [] キャプチャーリストが空のもの)にのみ有効です。

では std::function<int()> f の場合、型消去クラスを作成することになります。 std::function<int()> のインスタンスを作成し、(この場合) placement new を使用して size-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) (ただし、評価されていないコンテキストでラムダを作成することはできないので、上記は違法です。 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 !