Menurut sumber yang saya temukan, ekspresi lambda pada dasarnya diimplementasikan oleh kompilator yang membuat kelas dengan operator panggilan fungsi yang kelebihan beban dan variabel yang direferensikan sebagai anggota. Hal ini menunjukkan bahwa ukuran ekspresi lambda bervariasi, dan diberikan variabel referensi yang cukup sehingga ukurannya dapat menjadi besar secara sewenang-wenang .
An std::function
harus memiliki ukuran tetap , tetapi harus dapat membungkus segala jenis callable, termasuk lambda apa pun dari jenis yang sama. Bagaimana penerapannya? Jika secara std::function
internal menggunakan pointer ke targetnya, lalu apa yang terjadi, ketika std::function
instance disalin atau dipindahkan? Apakah ada alokasi heap yang terlibat?
std::function
beberapa waktu lalu. Ini pada dasarnya adalah kelas pegangan untuk objek polimorfik. Kelas turunan dari kelas dasar internal dibuat untuk menampung parameter, dialokasikan pada heap - kemudian penunjuk ke ini dipegang sebagai subobjek daristd::function
. Saya percaya itu menggunakan penghitungan referensi sepertistd::shared_ptr
untuk menangani penyalinan dan pemindahan.Jawaban:
Implementasi
std::function
dapat berbeda dari satu implementasi ke implementasi lainnya, tetapi ide intinya adalah menggunakan tipe-erasure. Meskipun ada banyak cara untuk melakukannya, Anda dapat membayangkan solusi yang sepele (tidak optimal) bisa seperti ini (disederhanakan untuk kasus khususstd::function<int (double)>
demi kesederhanaan):Dalam pendekatan sederhana ini,
function
objek hanya akan menyimpanunique_ptr
ke tipe dasar. Untuk setiap functor berbeda yang digunakan denganfunction
, tipe baru yang diturunkan dari basis dibuat dan objek dari tipe itu dibuat secara dinamis. Thestd::function
objek selalu dengan ukuran yang sama dan akan mengalokasikan ruang yang diperlukan untuk functors yang berbeda dalam tumpukan.Dalam kehidupan nyata, ada berbagai pengoptimalan yang memberikan keunggulan kinerja tetapi akan mempersulit jawabannya. Jenisnya dapat menggunakan pengoptimalan objek kecil, pengiriman dinamis dapat diganti dengan penunjuk fungsi bebas yang menggunakan functor sebagai argumen untuk menghindari satu tingkat tipuan ... tetapi idenya pada dasarnya sama.
Mengenai masalah bagaimana salinan
std::function
berperilaku, tes cepat menunjukkan bahwa salinan objek internal yang dapat dipanggil sudah selesai, daripada membagikan status.Pengujian tersebut menunjukkan bahwa
f2
mendapatkan salinan dari entitas yang dapat dipanggil, bukan referensi. Jika entitas yang dapat dipanggil dibagikan olehstd::function<>
objek yang berbeda , output dari program akan menjadi 5, 6, 7.sumber
std::function
akan benar jika objek internal disalin, dan saya rasa tidak demikian (pikirkan lambda yang menangkap nilai dan bisa berubah, disimpan di dalamstd::function
, jika status fungsi disalin jumlah salinanstd::function
di dalam algoritme standar dapat menghasilkan hasil yang berbeda, yang tidak diinginkan.std::function
akan memicu alokasi.Jawaban dari @David Rodríguez - dribeas bagus untuk mendemonstrasikan penghapusan jenis tetapi tidak cukup baik karena jenis-penghapusan juga mencakup bagaimana jenis disalin (dalam jawaban itu objek fungsi tidak akan dapat dibuat-salinan). Perilaku tersebut juga disimpan di
function
objek, selain data functor.Triknya, yang digunakan dalam implementasi STL dari Ubuntu 14.04 gcc 4.8, adalah menulis satu fungsi umum, mengkhususkan dengan setiap jenis fungsi yang memungkinkan, dan mentransmisikannya ke jenis penunjuk fungsi universal. Oleh karena itu informasi tipe dihapus .
Saya telah membuat versi sederhana dari itu. Semoga bisa membantu
Ada juga beberapa pengoptimalan dalam versi STL
construct_f
dandestroy_f
dicampur menjadi satu fungsi pointer (dengan parameter tambahan yang memberitahu apa yang harus dilakukan) untuk menyimpan beberapa byteunion
, sehingga ketikafunction
objek dibangun dari pointer fungsi, itu akan disimpan secara langsung di ruangunion
bukan heapMungkin implementasi STL bukanlah solusi terbaik karena saya pernah mendengar tentang implementasi yang lebih cepat . Namun saya yakin mekanisme dasarnya sama.
sumber
Untuk jenis argumen tertentu ("jika target f adalah objek yang dapat dipanggil yang diteruskan melalui
reference_wrapper
atau penunjuk fungsi"),std::function
konstruktor tidak mengizinkan pengecualian apa pun, jadi menggunakan memori dinamis bukanlah pertanyaan. Untuk kasus ini, semua data harus disimpan langsung di dalamstd::function
objek.Dalam kasus umum, (termasuk kasus lambda), menggunakan memori dinamis (baik melalui pengalokasi standar, atau pengalokasi yang diteruskan ke
std::function
konstruktor) diperbolehkan jika implementasi dirasa cocok. Standar merekomendasikan implementasi tidak menggunakan memori dinamis jika itu dapat dihindari, tetapi seperti yang Anda katakan dengan benar, jika objek fungsi (bukanstd::function
objek, tetapi objek yang dibungkus di dalamnya) cukup besar, tidak ada cara untuk mencegahnya, karenastd::function
memiliki ukuran tetap.Izin untuk membuang pengecualian ini diberikan ke konstruktor normal dan konstruktor salinan, yang secara cukup eksplisit memungkinkan alokasi memori dinamis selama penyalinan juga. Untuk bergerak, tidak ada alasan mengapa memori dinamis diperlukan. Standar tampaknya tidak secara eksplisit melarangnya, dan mungkin tidak bisa jika pemindahan dapat memanggil konstruktor pemindahan dari tipe objek yang dibungkus, tetapi Anda harus dapat berasumsi bahwa jika implementasi dan objek Anda masuk akal, pemindahan tidak akan menyebabkan alokasi apa pun.
sumber
An
std::function
membebanioperator()
menjadikannya sebagai objek functor, lambda bekerja dengan cara yang sama. Ini pada dasarnya membuat struct dengan variabel anggota yang dapat diakses di dalamoperator()
fungsi. Jadi konsep dasar yang perlu diingat adalah bahwa lambda adalah objek (disebut fungsi atau fungsi) bukan fungsi. Standar tersebut mengatakan untuk tidak menggunakan memori dinamis jika dapat dihindari.sumber
std::function
? Itulah pertanyaan kuncinya di sini.std::function
objek memiliki ukuran yang sama, dan bukan ukuran lambda yang terkandung.std::vector<T...>
objek memiliki ukuran tetap (waktu-waktu) tetap tidak bergantung pada instance / jumlah elemen pengalokasi aktual.std::function<void ()> f;
tidak perlu mengalokasikan di sana,std::function<void ()> f = [&]() { /* captures tons of variables */ };
kemungkinan besar mengalokasikan.std::function<void()> f = &free_function;
mungkin juga tidak mengalokasikan ...