Mengapa lambda memiliki ukuran 1 byte?

89

Saya bekerja dengan memori beberapa lambda di C ++, tapi saya agak bingung dengan ukurannya.

Ini kode tes saya:

#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;
}

Anda dapat menjalankannya di sini: http://fiddle.jyt.io/github/b13f682d1237eb69ebdc60728bb52598

Ouptutnya adalah:

17
0x7d90ba8f626f
1

Ini menunjukkan bahwa ukuran lambda saya adalah 1.

  • Bagaimana ini mungkin?

  • Bukankah sebaiknya lambda menjadi, setidaknya, penunjuk untuk implementasinya?

sdgfsdh
sumber
17
itu diimplementasikan sebagai objek fungsi (a structdengan operator())
george_ptr
14
Dan struct kosong tidak bisa berukuran 0 maka hasilnya 1. Coba tangkap sesuatu dan lihat apa yang terjadi pada ukurannya.
Mohamad Elghawi
2
Mengapa lambda harus menjadi penunjuk ??? Itu adalah objek yang memiliki operator panggilan.
Kerrek SB
7
Lambdas di C ++ ada pada waktu kompilasi, dan pemanggilan ditautkan (atau bahkan sebaris) pada waktu kompilasi atau tautan. Oleh karena itu tidak perlu untuk runtime pointer dalam objek itu sendiri. @KerrekSB Bukan tebakan yang tidak wajar untuk berharap bahwa lambda akan berisi penunjuk fungsi, karena kebanyakan bahasa yang mengimplementasikan lambda lebih dinamis daripada C ++.
Kyle Strand
2
@KerrekSB "yang penting" - dalam arti apa? The Alasan objek penutupan bisa kosong (bukan yang mengandung fungsi pointer) adalah karena fungsi untuk dipanggil diketahui pada waktu kompilasi / link. Inilah yang tampaknya salah dipahami oleh OP. Saya tidak melihat bagaimana komentar Anda mengklarifikasi banyak hal.
Kyle Strand

Jawaban:

107

Lambda yang dimaksud sebenarnya tidak memiliki status .

Memeriksa:

struct lambda {
  auto operator()() const { return 17; }
};

Dan jika kita punya lambda f;, itu adalah kelas kosong. Tidak hanya di atas secara lambdafungsional mirip dengan lambda Anda, itu (pada dasarnya) bagaimana lambda Anda diimplementasikan! (Ini juga membutuhkan operator implisit cast to function pointer, dan namanya lambdaakan diganti dengan beberapa pseudo-guid yang dihasilkan kompiler)

Dalam C ++, objek bukanlah pointer. Itu adalah hal yang nyata. Mereka hanya menggunakan ruang yang dibutuhkan untuk menyimpan data di dalamnya. Penunjuk ke sebuah objek bisa lebih besar dari sebuah objek.

Meskipun Anda mungkin menganggap lambda itu sebagai penunjuk ke suatu fungsi, sebenarnya tidak. Anda tidak dapat menetapkan ulang auto f = [](){ return 17; };ke fungsi atau lambda yang berbeda!

 auto f = [](){ return 17; };
 f = [](){ return -42; };

di atas adalah ilegal . Tidak ada ruang di ftoko mana fungsi akan disebut - bahwa informasi disimpan dalam jenis dari f, tidak dalam nilai f!

Jika Anda melakukan ini:

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

atau ini:

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

Anda tidak lagi menyimpan lambda secara langsung. Dalam kedua kasus ini, f = [](){ return -42; }adalah legal - jadi dalam kasus ini, kami menyimpan fungsi mana yang kami panggil nilainya f. Dan sizeof(f)tidak lagi 1, melainkan lebih sizeof(int(*)())atau lebih besar (pada dasarnya, berukuran penunjuk atau lebih besar, seperti yang Anda harapkan. std::functionMemiliki ukuran min yang tersirat oleh standar (mereka harus dapat menyimpan callable "di dalam diri mereka sendiri" hingga ukuran tertentu) yang setidaknya sebesar fungsi pointer dalam praktiknya).

Dalam int(*f)()kasus ini, Anda menyimpan penunjuk fungsi ke fungsi yang berperilaku seolah-olah Anda memanggil lambda itu. Ini hanya berfungsi untuk lambda tanpa negara (yang memiliki []daftar tangkapan kosong ).

Dalam std::function<int()> fkasus ini, Anda membuat std::function<int()>instance kelas penghapusan tipe yang (dalam kasus ini) menggunakan penempatan baru untuk menyimpan salinan lambda size-1 dalam buffer internal (dan, jika lambda yang lebih besar diteruskan (dengan lebih banyak status ), akan menggunakan alokasi heap).

Sebagai tebakan, hal seperti ini mungkin yang menurut Anda sedang terjadi. Itu lambda adalah objek yang tipenya dijelaskan oleh tanda tangannya. Dalam C ++, diputuskan untuk membuat abstraksi biaya nol lambda atas implementasi objek fungsi manual. Ini memungkinkan Anda meneruskan lambda ke dalam stdalgoritme (atau yang serupa) dan membuat kontennya terlihat sepenuhnya oleh kompiler saat membuat instance template algoritme. Jika lambda memiliki tipe seperti std::function<void(int)>, isinya tidak akan terlihat sepenuhnya, dan objek fungsi buatan tangan mungkin lebih cepat.

Tujuan dari standardisasi C ++ adalah pemrograman tingkat tinggi dengan overhead nol pada kode C buatan tangan.

Sekarang setelah Anda memahami bahwa Anda fsebenarnya tidak memiliki kewarganegaraan, seharusnya ada pertanyaan lain di kepala Anda: lambda tidak memiliki status. Mengapa tidak ada ukuran 0?


Ada jawaban singkatnya.

Semua objek di C ++ harus memiliki ukuran minimal 1 di bawah standar, dan dua objek dengan jenis yang sama tidak boleh memiliki alamat yang sama. Ini terhubung, karena array tipe Takan memiliki elemen yang ditempatkan sizeof(T)terpisah.

Sekarang, karena tidak memiliki status, terkadang tidak memakan tempat. Hal ini tidak dapat terjadi jika "sendirian", tetapi dalam beberapa konteks dapat terjadi. std::tupledan kode pustaka serupa memanfaatkan fakta ini. Berikut cara kerjanya:

Karena lambda setara dengan kelas dengan operator()beban berlebih, lambda tanpa status (dengan []daftar tangkapan) adalah semua kelas kosong. Mereka memiliki sizeofdari 1. Faktanya, jika Anda mewarisi dari mereka (yang diperbolehkan!), Mereka tidak akan memakan tempat selama tidak menyebabkan benturan alamat tipe yang sama . (Ini dikenal sebagai pengoptimalan basis kosong).

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

the sizeof(make_toy( []{std::cout << "hello world!\n"; } ))is sizeof(int)(di atas adalah ilegal karena Anda tidak dapat membuat lambda dalam konteks yang tidak dievaluasi: Anda harus membuat nama auto toy = make_toy(blah);lalu lakukan sizeof(blah), tetapi itu hanya noise). sizeof([]{std::cout << "hello world!\n"; })masih 1(kualifikasi serupa).

Jika kita membuat jenis mainan lain:

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

ini memiliki dua salinan lambda. Karena mereka tidak dapat berbagi alamat yang sama, sizeof(toy2(some_lambda))is 2!

Yakk - Adam Nevraumont
sumber
6
Nit: Penunjuk fungsi bisa lebih kecil dari void *. Dua contoh sejarah: Kata pertama yang dialamatkan mesin di mana sizeof (void *) == sizeof (char *)> sizeof (struct *) == sizeof (int *). (void * dan char * membutuhkan beberapa bit ekstra untuk menahan offset dalam satu kata). Kedua model memori 8086 di mana void * / int * adalah segmen + offset dan dapat mencakup semua memori, tetapi fungsi dipasang dalam satu segmen 64K ( jadi function pointer hanya 16 bit).
Martin Bonner mendukung Monica
1
@martin benar. Ekstra ()ditambahkan.
Yakk - Adam Nevraumont
50

Lambda bukanlah penunjuk fungsi.

Lambda adalah turunan dari kelas. Kode Anda kira-kira sama dengan:

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;

Kelas internal yang mewakili lambda tidak memiliki anggota kelas, oleh karena itu kelasnya sizeof()adalah 1 (tidak boleh 0, karena alasan yang dinyatakan secara memadai di tempat lain ).

Jika lambda Anda menangkap beberapa variabel, mereka akan setara dengan anggota kelas, dan Anda sizeof()akan menunjukkannya.

Sam Varshavchik
sumber
3
Bisakah Anda menautkan ke "di tempat lain", yang menjelaskan mengapa sizeof()tidak boleh 0?
pengguna1717828
26

Kompiler Anda kurang lebih menerjemahkan lambda ke tipe struct berikut:

struct _SomeInternalName {
    int operator()() { return 17; }
};

int main()
{
     _SomeInternalName f;
     std::cout << f() << std::endl;
}

Karena struct tersebut tidak memiliki anggota non-statis, ia memiliki ukuran yang sama dengan struct kosong, yaitu 1.

Itu berubah segera setelah Anda menambahkan daftar tangkapan yang tidak kosong ke lambda Anda:

int i = 42;
auto f = [i]() { return i; };

Yang akan diterjemahkan ke

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

Karena struct yang dihasilkan sekarang perlu menyimpan anggota non-statis intuntuk penangkapan, ukurannya akan bertambah sizeof(int). Ukurannya akan terus bertambah saat Anda menangkap lebih banyak barang.

(Silakan ambil analogi struct dengan sebutir garam. Meskipun ini adalah cara yang bagus untuk menjelaskan tentang bagaimana lambda bekerja secara internal, ini bukan terjemahan literal dari apa yang akan dilakukan kompilator)

ComicSansMS
sumber
12

Bukankah lambda harus, di mimumum, pointer ke implementasinya?

Belum tentu. Menurut standar, ukuran kelas yang unik dan tidak bernama ditentukan oleh implementasi . Kutipan dari [expr.prim.lambda] , C ++ 14 (penekanan saya):

Tipe ekspresi lambda (yang juga merupakan tipe objek closure) adalah tipe kelas nonunion yang unik dan tidak bernama - disebut tipe closure - yang propertinya dijelaskan di bawah ini.

[...]

Implementasi dapat mendefinisikan tipe closure secara berbeda dari apa yang dijelaskan di bawah ini asalkan ini tidak mengubah perilaku program yang dapat diamati selain dengan mengubah :

- ukuran dan / atau kesejajaran tipe penutupan ,

- apakah tipe closure dapat disalin dengan mudah (Klausul 9),

- apakah tipe closure adalah kelas tata letak standar (Klausul 9), atau

- apakah tipe closure adalah kelas POD (Klausul 9)

Dalam kasus Anda - untuk kompiler yang Anda gunakan - Anda mendapatkan ukuran 1, yang tidak berarti itu sudah diperbaiki. Ini dapat bervariasi antara implementasi compiler yang berbeda.

legends2k
sumber
Apakah Anda yakin bit ini berlaku? Lambda tanpa grup penangkap sebenarnya bukanlah "penutup". (Apakah standar merujuk pada lambda kelompok tangkap kosong sebagai "penutupan"?)
Kyle Strand
1
Ya, benar. Ini adalah apa yang dikatakan standar " Evaluasi ekspresi lambda menghasilkan nilai sementara. Sementara ini disebut objek penutupan. ", Menangkap atau tidak, itu adalah objek penutup, hanya saja nilai upnya akan kosong.
legends2k
Saya tidak downvote, tetapi mungkin downvoter tidak menganggap jawaban ini berharga karena tidak menjelaskan mengapa mungkin (dari perspektif teoritis, bukan perspektif standar) untuk mengimplementasikan lambda tanpa menyertakan penunjuk run-time ke fungsi operator panggilan. (Lihat diskusi saya dengan KerrekSB di bawah pertanyaan.)
Kyle Strand
7

Dari http://en.cppreference.com/w/cpp/language/lambda :

Ekspresi lambda membangun objek sementara prvalue tak bernama dari tipe kelas non-gabungan non-gabungan unik tak bernama, yang dikenal sebagai tipe penutupan , yang dideklarasikan (untuk tujuan ADL) dalam cakupan blok terkecil, cakupan kelas, atau ruang lingkup namespace yang berisi ekspresi lambda.

Jika ekspresi lambda menangkap apa pun dengan salinan (baik secara implisit dengan klausa penangkapan [=] atau secara eksplisit dengan tangkapan yang tidak menyertakan karakter &, misalnya [a, b, c]), tipe penutupan menyertakan data non-statis tanpa nama anggota , yang dideklarasikan dalam urutan yang tidak ditentukan, yang menyimpan salinan dari semua entitas yang ditangkap.

Untuk entitas yang ditangkap dengan referensi (dengan tangkapan default [&] atau saat menggunakan karakter &, misalnya [& a, & b, & c]), tidak ditentukan jika anggota data tambahan dideklarasikan dalam tipe penutupan

Dari http://en.cppreference.com/w/cpp/language/sizeof

Ketika diterapkan ke tipe kelas kosong, selalu mengembalikan 1.

george_ptr
sumber