Pertimbangkan program yang cukup tidak berguna ini:
#include <iostream>
int main(int argc, char* argv[]) {
int a = 5;
auto it = [&](auto self) {
return [&](auto b) {
std::cout << (a + b) << std::endl;
return self(self);
};
};
it(it)(4)(6)(42)(77)(999);
}
Pada dasarnya kami mencoba membuat lambda yang kembali dengan sendirinya.
- MSVC mengkompilasi program, dan menjalankannya
- gcc mengkompilasi program, dan itu segfault
- clang menolak program dengan pesan:
error: function 'operator()<(lambda at lam.cpp:6:13)>' with deduced return type cannot be used before it is defined
Kompiler mana yang benar? Apakah ada pelanggaran kendala statis, UB, atau tidak keduanya?
Perbarui sedikit modifikasi ini diterima oleh dentang:
auto it = [&](auto& self, auto b) {
std::cout << (a + b) << std::endl;
return [&](auto p) { return self(self,p); };
};
it(it,4)(6)(42)(77)(999);
Pembaruan 2 : Saya mengerti bagaimana menulis sebuah functor yang mengembalikan dirinya sendiri, atau bagaimana menggunakan kombinator Y, untuk mencapai ini. Ini lebih merupakan pertanyaan pengacara bahasa.
Pembaruan 3 : pertanyaannya bukanlah apakah legal bagi lambda untuk mengembalikan dirinya sendiri secara umum, tetapi tentang legalitas cara khusus untuk melakukan ini.
Pertanyaan terkait: C ++ lambda kembali sendiri .
auto& self
yang menghilangkan masalah referensi yang menjuntai.Jawaban:
Program tidak berbentuk (clang is right) menurut [dcl.spec.auto] / 9 :
Pada dasarnya, pengurangan tipe kembalian lambda dalam bergantung pada dirinya sendiri (entitas yang dinamai di sini adalah operator panggilan) - jadi Anda harus secara eksplisit menyediakan tipe kembalian. Dalam kasus khusus ini, itu tidak mungkin, karena Anda memerlukan jenis lambda bagian dalam tetapi tidak dapat memberi nama. Tetapi ada kasus lain di mana mencoba memaksa lambda rekursif seperti ini, yang bisa berhasil.
Bahkan tanpa itu, Anda memiliki referensi yang menggantung .
Izinkan saya menjelaskan lebih lanjut, setelah berdiskusi dengan seseorang yang jauh lebih pintar (yaitu TC) Ada perbedaan penting antara kode asli (sedikit dikurangi) dan versi baru yang diusulkan (juga dikurangi):
Dan itu adalah ekspresi batin
self(self)
tidak bergantungf1
, tetapiself(self, p)
bergantung padaf2
. Ketika ekspresi non-dependen, ekspresi tersebut dapat digunakan ... dengan penuh semangat ( [temp.res] / 8 , misalnya bagaimanastatic_assert(false)
hard error terlepas dari apakah template yang ditemukannya itu dibuat instance-nya atau tidak).Sebab
f1
, kompiler (seperti, katakanlah, clang) dapat mencoba membuat instance ini dengan bersemangat. Anda tahu jenis deduksi lambda luar setelah Anda mencapai itu;
pada poin di#2
atas (ini adalah tipe lambda bagian dalam), tetapi kami mencoba menggunakannya lebih awal dari itu (anggap saja pada titik#1
) - kami sedang mencoba untuk menggunakannya saat kita masih mengurai lambda bagian dalam, sebelum kita mengetahui jenis sebenarnya. Itu bertentangan dengan dcl.spec.auto/9.Namun, karena
f2
, kita tidak bisa mencoba membuat instantiate dengan bersemangat, karena itu tergantung. Kita hanya dapat memberi contoh pada titik penggunaan, yang pada saat itu kita mengetahui segalanya.Untuk benar-benar melakukan sesuatu seperti ini, Anda memerlukan kombinator-y . Implementasi dari makalah:
Dan yang Anda inginkan adalah:
sumber
Sunting : Tampaknya ada beberapa kontroversi mengenai apakah konstruksi ini benar-benar valid sesuai spesifikasi C ++. Pendapat yang berlaku sepertinya tidak valid. Lihat jawaban lainnya untuk pembahasan yang lebih menyeluruh. Sisa dari jawaban ini berlaku jika konstruksinya valid; kode tweak di bawah ini bekerja dengan MSVC ++ dan gcc, dan OP telah memposting kode yang dimodifikasi lebih lanjut yang bekerja dengan clang juga.
Ini adalah perilaku tidak terdefinisi, karena lambda bagian dalam menangkap parameter
self
dengan referensi, tetapiself
keluar dari cakupan setelahreturn
di baris 7. Jadi, ketika lambda yang dikembalikan dijalankan nanti, itu mengakses referensi ke variabel yang telah keluar dari ruang lingkup.Menjalankan program dengan
valgrind
menggambarkan ini:Alih-alih, Anda dapat mengubah lambda luar untuk mengambil sendiri dengan referensi alih-alih berdasarkan nilai, sehingga menghindari banyak salinan yang tidak perlu dan juga menyelesaikan masalah:
Ini bekerja:
sumber
self
referensi?self
referensi, masalah ini akan hilang , tetapi Clang masih menolaknya karena alasan lainself
itu ditangkap dengan referensi!TL; DR;
dentang benar.
Sepertinya bagian dari standar yang membuat bentuk buruk ini adalah [dcl.spec.auto] p9 :
Pekerjaan asli melalui
Jika kita melihat proposal A Proposal untuk Menambahkan Y Combinator ke Perpustakaan Standar, ini memberikan solusi yang berfungsi:
dan secara eksplisit mengatakan contoh Anda tidak mungkin:
dan merujuk pada diskusi di mana Richard Smith menyinggung kesalahan yang diberikan oleh clang kepada Anda :
Barry mengarahkan saya ke proposal tindak lanjut Lambda rekursif yang menjelaskan mengapa hal ini tidak mungkin dan bekerja di sekitar
dcl.spec.auto#9
batasan dan juga menunjukkan metode untuk mencapai ini hari ini tanpanya:sumber
self
Sepertinya entitas tersebut tidak.Sepertinya dentang itu benar. Pertimbangkan contoh yang disederhanakan:
Mari kita bahas seperti kompiler (sedikit):
it
adalahLambda1
dengan operator panggilan template.it(it);
memicu instansiasi operator panggilanauto
, jadi kita harus menyimpulkannya.Lambda1
.self(self)
self(self)
persis seperti yang kita mulai!Dengan demikian, jenisnya tidak dapat disimpulkan.
sumber
Lambda1::operator()
sederhanaLambda2
. Kemudian dalam ekspresi lambda bagian dalam itu, jenis kembalian dariself(self)
, panggilan dariLambda1::operator()
, juga dikenalLambda2
. Mungkin aturan formal menghalangi pembuatan deduksi sepele itu, tetapi logika yang disajikan di sini tidak. Logikanya di sini hanyalah sebuah pernyataan. Jika aturan formal benar-benar menghalangi, maka itu cacat dalam aturan formal.Nah, kode Anda tidak berfungsi. Tapi ini memang:
Kode tes:
Kode Anda adalah UB dan berformat buruk, tidak diperlukan diagnostik. Yang lucu; tetapi keduanya bisa diperbaiki secara independen.
Pertama, UB:
ini adalah UB karena bagian luar diambil
self
oleh nilai, kemudian bagian dalam ditangkapself
dengan referensi, kemudian dilanjutkan untuk mengembalikannya setelahouter
selesai dijalankan. Jadi segfaulting sudah pasti ok.Cara mengatasinya:
Kode tetap berbentuk buruk. Untuk melihat ini, kami dapat memperluas lambda:
ini contoh
__outer_lambda__::operator()<__outer_lambda__>
:Jadi selanjutnya kita harus menentukan jenis kembalian
__outer_lambda__::operator()
.Kami melewatinya baris demi baris. Pertama kita buat
__inner_lambda__
tipe:Sekarang, lihat di sana - jenis kembaliannya adalah
self(self)
, atau__outer_lambda__(__outer_lambda__ const&)
. Tapi kami sedang mencoba menyimpulkan jenis pengembalian__outer_lambda__::operator()(__outer_lambda__)
.Anda tidak diizinkan melakukan itu.
Meskipun pada kenyataannya tipe kembalian
__outer_lambda__::operator()(__outer_lambda__)
sebenarnya tidak bergantung pada tipe kembalian__inner_lambda__::operator()(int)
, C ++ tidak peduli saat mengurangi tipe kembalian; itu hanya memeriksa kode baris demi baris.Dan
self(self)
digunakan sebelum kami menyimpulkannya. Program yang buruk.Kita bisa menambalnya dengan bersembunyi
self(self)
sampai nanti:dan sekarang kodenya benar dan terkompilasi. Tapi saya pikir ini sedikit hack; cukup gunakan ycombinator.
sumber
operator()
dalam yang diberi templated , secara umum tidak dapat disimpulkan sampai itu dipakai (dengan dipanggil dengan beberapa argumen dari beberapa tipe). Jadi penulisan ulang seperti mesin manual ke kode berbasis template bekerja dengan baik.Cukup mudah untuk menulis ulang kode dalam istilah kelas yang kompilator akan, atau lebih tepatnya, menghasilkan untuk ekspresi lambda.
Setelah selesai, jelas bahwa masalah utamanya hanyalah referensi yang menggantung, dan kompiler yang tidak menerima kode tersebut agak tertantang di departemen lambda.
Penulisan ulang menunjukkan bahwa tidak ada dependensi melingkar.
Versi template penuh untuk mencerminkan cara lambda bagian dalam dalam kode asli, menangkap item yang berjenis template:
Saya rasa template dalam mesin internal inilah, yang dirancang untuk dilarang oleh aturan formal. Jika mereka melarang konstruksi aslinya.
sumber
template< class > class Inner;
templateoperator()
itu ... dipakai? Yah, salah kata. Tertulis? ... selamaOuter::operator()<Outer>
sebelum jenis pengembalian operator luar disimpulkan. DanInner<Outer>::operator()
memiliki panggilan untukOuter::operator()<Outer>
dirinya sendiri. Dan itu tidak diperbolehkan. Sekarang, sebagian besar kompiler tidak melihat ituself(self)
karena mereka menunggu untuk menyimpulkan jenis kembalinyaOuter::Inner<Outer>::operator()<int>
ketikaint
dilewatkan dalam. Sensible. Tapi itu merindukan kelemahan kode itu.Innner<T>::operator()<U>
,, dipakai. Bagaimanapun, jenis pengembalian bisa bergantung diU
sini. Tidak, tapi secara umum.