Saya mencoba untuk memahami / mengklarifikasi kode kode yang dihasilkan ketika tangkapan dilewatkan ke lambdas terutama di tangkapan init umum yang ditambahkan dalam C ++ 14.
Berikan contoh kode berikut yang tercantum di bawah ini adalah pemahaman saya saat ini tentang apa yang akan dihasilkan oleh kompiler.
Kasus 1: ditangkap dengan nilai / tangkapan standar dengan nilai
int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };
Akan sama dengan:
class __some_compiler_generated_name {
public:
__some_compiler_generated_name(int x) : __x{x}{}
void operator()() const { std::cout << __x << std::endl;}
private:
int __x;
};
Jadi ada banyak salinan, satu untuk menyalin ke parameter konstruktor dan satu untuk menyalin ke anggota, yang akan mahal untuk jenis seperti vektor dll.
Kasus 2: penangkapan dengan referensi / penangkapan standar dengan referensi
int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };
Akan sama dengan:
class __some_compiler_generated_name {
public:
__some_compiler_generated_name(int& x) : x_{x}{}
void operator()() const { std::cout << x << std::endl;}
private:
int& x_;
};
Parameter adalah referensi dan anggota adalah referensi sehingga tidak ada salinan. Bagus untuk jenis seperti vektor dll.
Kasus 3:
Tangkapan init umum
auto lambda = [x = 33]() { std::cout << x << std::endl; };
Posisi saya adalah ini mirip dengan Kasus 1 dalam arti disalin ke anggota.
Dugaan saya adalah bahwa kompiler menghasilkan kode yang mirip dengan ...
class __some_compiler_generated_name {
public:
__some_compiler_generated_name() : __x{33}{}
void operator()() const { std::cout << __x << std::endl;}
private:
int __x;
};
Juga jika saya memiliki yang berikut ini:
auto l = [p = std::move(unique_ptr_var)]() {
// do something with unique_ptr_var
};
Seperti apa konstruktornya? Apakah itu juga memindahkannya ke anggota?
Jawaban:
Pertanyaan ini tidak dapat dijawab sepenuhnya dalam kode. Anda mungkin dapat menulis kode yang agak "setara", tetapi standarnya tidak ditentukan seperti itu.
Dengan itu, mari selami
[expr.prim.lambda]
. Hal pertama yang perlu diperhatikan adalah konstruktor hanya disebutkan dalam[expr.prim.lambda.closure]/13
:Jadi, langsung kelelawar, harus jelas bahwa konstruktor tidak secara formal bagaimana menangkap objek didefinisikan. Anda bisa mendapatkan cukup dekat (lihat jawaban cppinsights.io), tetapi detailnya berbeda (perhatikan bagaimana kode dalam jawaban untuk kasus 4 tidak dikompilasi).
Ini adalah klausa standar utama yang diperlukan untuk membahas kasus 1:
[expr.prim.lambda.capture]/10
[expr.prim.lambda.capture]/11
[expr.prim.lambda.capture]/15
Mari kita terapkan ini pada kasus Anda 1:
Tipe penutupan lambda ini akan memiliki anggota data non-statis yang tidak disebutkan namanya (sebut saja
__x
) tipeint
(karenax
bukan merupakan referensi atau fungsi), dan akses kex
dalam tubuh lambda ditransformasikan menjadi akses ke__x
. Ketika kami mengevaluasi ekspresi lambda (yaitu ketika menetapkanlambda
), kami langsung menginisialisasi__x
denganx
.Singkatnya, hanya satu salinan yang terjadi . Konstruktor tipe penutupan tidak terlibat, dan tidak mungkin untuk menyatakan ini dalam C ++ normal (perhatikan bahwa tipe penutupan juga bukan tipe agregat ).
Pengambilan referensi meliputi
[expr.prim.lambda.capture]/12
:Ada paragraf lain tentang penangkapan referensi, tetapi kami tidak melakukannya di mana pun.
Jadi, untuk kasus 2:
Kami tidak tahu apakah anggota ditambahkan ke jenis penutupan.
x
dalam tubuh lambda mungkin langsung merujuk kex
luar. Ini tergantung pada kompilator untuk mencari tahu, dan itu akan melakukan ini dalam beberapa bentuk bahasa perantara (yang berbeda dari kompiler ke kompiler), bukan sumber transformasi dari kode C ++.Pengambilan init dirinci dalam
[expr.prim.lambda.capture]/6
:Mengingat itu, mari kita lihat kasus 3:
Seperti yang dinyatakan, bayangkan ini sebagai variabel yang dibuat oleh
auto x = 33;
dan ditangkap secara eksplisit oleh salinan. Variabel ini hanya "terlihat" di dalam tubuh lambda. Seperti disebutkan[expr.prim.lambda.capture]/15
sebelumnya, inisialisasi anggota yang sesuai dari tipe penutupan (__x
untuk anak cucu) adalah oleh penginisialisasi yang diberikan pada evaluasi ekspresi lambda.Untuk menghindari keraguan: Ini tidak berarti segala sesuatu diinisialisasi dua kali di sini. Ini
auto x = 33;
adalah "seolah-olah" untuk mewarisi semantik menangkap sederhana, dan inisialisasi yang dijelaskan adalah modifikasi untuk semantik tersebut. Hanya satu inisialisasi terjadi.Ini juga mencakup kasus 4:
Anggota tipe penutupan diinisialisasi oleh
__p = std::move(unique_ptr_var)
ketika ekspresi lambda dievaluasi (yaitu ketikal
ditugaskan untuk). Akses kep
dalam tubuh lambda diubah menjadi akses ke__p
.TL; DR: Hanya jumlah minimal salinan / inisialisasi / gerakan yang dilakukan (seperti yang diharapkan / diharapkan). Saya akan berasumsi bahwa lambda tidak ditentukan dalam hal transformasi sumber (tidak seperti gula sintaksis lainnya) persis karena mengungkapkan hal-hal dalam hal konstruktor akan memerlukan operasi yang berlebihan.
Saya harap ini menyelesaikan ketakutan yang diungkapkan dalam pertanyaan :)
sumber
Kasus 1
[x](){}
: Konstruktor yang dihasilkan akan menerima argumennya denganconst
referensi yang mungkin memenuhi syarat untuk menghindari salinan yang tidak perlu:Kasus 2
[x&](){}
: Asumsi Anda di sini benar,x
diteruskan dan disimpan dengan referensi.Kasus 3
[x = 33](){}
: Sekali lagi benar,x
diinisialisasi oleh nilai.Kasus 4
[p = std::move(unique_ptr_var)]
: Konstruktor akan terlihat seperti ini:jadi ya,
unique_ptr_var
itu "pindah ke" penutupan. Lihat juga Item 32 Scott Meyer di C ++ Modern Efektif ("Gunakan tangkapan init untuk memindahkan objek ke penutupan").sumber
const
-Kualifikasi" Kenapa?const
tidak ada salahnya di sini karena beberapa ambiguitas / kecocokan yang lebih baik ketika tidakconst
dll. Lagi pula, apakah Anda pikir saya harus menghapusconst
?Ada sedikit kebutuhan untuk berspekulasi, menggunakan cppinsights.io .
Kasus 1:
Kode
Kompiler menghasilkan
Kasus 2:
Kode
Kompiler menghasilkan
Kasus 3:
Kode
Kompiler menghasilkan
Kasus 4 (tidak resmi):
Kode
Kompiler menghasilkan
Dan saya percaya potongan kode terakhir ini menjawab pertanyaan Anda. Pergerakan terjadi, tetapi tidak [secara teknis] di konstruktor.
Menangkap sendiri tidak
const
, tetapi Anda dapat melihat bahwaoperator()
fungsinya. Tentu, jika Anda perlu memodifikasi tangkapan, Anda menandai lambda sebagaimutable
.sumber