C ++ Pembuatan Kode Lambda dengan Capture Init di C ++ 14

9

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?

Blair Davidson
sumber
1
@ rafix07 Jika demikian, kode wawasan yang dihasilkan bahkan tidak dapat dikompilasi (mencoba menyalin-menginisialisasi anggota ptr unik dari argumen). cppinsights berguna untuk mendapatkan intisari umum, tetapi jelas tidak dapat menjawab pertanyaan di sini.
Max Langhof
Anda tampaknya menganggap ada terjemahan lambda's ke functors sebagai langkah pertama kompilasi, atau apakah Anda hanya mencari kode yang setara (mis. Perilaku yang sama)? Cara kompiler tertentu menghasilkan kode (dan kode mana yang dihasilkannya) akan bergantung pada kompiler, versi, arsitektur, bendera, dll. Jadi, apakah Anda meminta platform tertentu? Jika tidak, pertanyaan Anda tidak benar-benar dapat dijawab. Selain dari kode aktual yang dihasilkan mungkin akan lebih efisien daripada fungsi yang Anda daftarkan (mis. Konstruktor inline, menghindari salinan yang tidak perlu, dll.).
Sander De Dycker
2
Jika Anda tertarik dengan apa yang dikatakan standar C ++ tentang hal itu, lihat [expr.prim.lambda] . Terlalu banyak untuk diringkas di sini sebagai jawaban.
Sander De Dycker

Jawaban:

2

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:

Jenis penutupan terkait dengan lambda ekspresi tidak memiliki konstruktor default jika lambda ekspresi memiliki lambda-capture dan konstruktor default macet sebaliknya. Ini memiliki konstruktor salin default dan konstruktor pemindahan default ([class.copy.ctor]). Ini memiliki operator copy tugas dihapus jika lambda ekspresi memiliki lambda-capture dan macet copy dan operator penugasan bergerak sebaliknya ([class.copy.assign]). [ Catatan: Fungsi-fungsi anggota khusus ini secara implisit didefinisikan seperti biasa, dan karenanya dapat didefinisikan sebagai dihapus. - catatan akhir ]

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

[...]
Untuk setiap entitas yang ditangkap melalui salinan, anggota data non-statis yang tidak disebutkan namanya dinyatakan dalam tipe penutupan. Urutan deklarasi anggota ini tidak ditentukan. Jenis anggota data tersebut adalah tipe yang direferensikan jika entitas merupakan referensi ke objek, referensi nilai untuk tipe fungsi yang direferensikan jika entitas tersebut merupakan referensi ke suatu fungsi, atau jenis entitas yang ditangkap yang sesuai sebaliknya. Seorang anggota serikat anonim tidak akan ditangkap melalui salinan.

[expr.prim.lambda.capture]/11

Setiap ekspresi-id di dalam senyawa-pernyataan dari ekspresi lambda yang merupakan penggunaan odr dari entitas yang ditangkap oleh salinan ditransformasikan menjadi akses ke anggota data yang tidak disebutkan namanya dari tipe penutupan. [...]

[expr.prim.lambda.capture]/15

Ketika ekspresi lambda dievaluasi, entitas yang ditangkap oleh salinan digunakan untuk menginisialisasi langsung setiap anggota data non-statis terkait dari objek penutupan yang dihasilkan, dan anggota data non-statis yang sesuai dengan init-captures diinisialisasi sebagai ditunjukkan oleh penginisialisasi yang sesuai (yang dapat berupa inisialisasi salin atau langsung). [...]

Mari kita terapkan ini pada kasus Anda 1:

Kasus 1: ditangkap dengan nilai / tangkapan standar dengan nilai

int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };

Tipe penutupan lambda ini akan memiliki anggota data non-statis yang tidak disebutkan namanya (sebut saja __x) tipe int(karena xbukan merupakan referensi atau fungsi), dan akses ke xdalam tubuh lambda ditransformasikan menjadi akses ke __x. Ketika kami mengevaluasi ekspresi lambda (yaitu ketika menetapkan lambda), kami langsung menginisialisasi __x dengan x.

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:

Suatu entitas ditangkap dengan referensi jika secara implisit atau eksplisit ditangkap tetapi tidak ditangkap oleh salinan. Tidak ditentukan apakah anggota data non-statis tambahan yang tidak disebutkan namanya dinyatakan dalam tipe penutupan untuk entitas yang ditangkap oleh referensi. [...]

Ada paragraf lain tentang penangkapan referensi, tetapi kami tidak melakukannya di mana pun.

Jadi, untuk kasus 2:

Kasus 2: penangkapan dengan referensi / penangkapan standar dengan referensi

int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };

Kami tidak tahu apakah anggota ditambahkan ke jenis penutupan. xdalam tubuh lambda mungkin langsung merujuk ke xluar. 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:

Tangkapan init berperilaku seolah-olah mendeklarasikan dan secara eksplisit menangkap variabel dari bentuk auto init-capture ;yang wilayah deklaratifnya adalah pernyataan gabungan lambda-ekspresi, kecuali bahwa:

  • (6.1) jika penangkapan dilakukan dengan menyalin (lihat di bawah), anggota data non-statis yang dinyatakan untuk penangkapan dan variabel diperlakukan sebagai dua cara berbeda untuk merujuk ke objek yang sama, yang memiliki masa pakai data non-statis anggota, dan tidak ada salinan dan penghancuran tambahan dilakukan, dan
  • (6.2) jika tangkapan adalah dengan referensi, masa hidup variabel berakhir ketika masa objek penutupan berakhir.

Mengingat itu, mari kita lihat kasus 3:

Kasus 3: Penangkapan init umum

auto lambda = [x = 33]() { std::cout << x << std::endl; };

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]/15sebelumnya, inisialisasi anggota yang sesuai dari tipe penutupan ( __xuntuk 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:

auto l = [p = std::move(unique_ptr_var)]() {
  // do something with unique_ptr_var
};

Anggota tipe penutupan diinisialisasi oleh __p = std::move(unique_ptr_var)ketika ekspresi lambda dievaluasi (yaitu ketika lditugaskan untuk). Akses ke pdalam 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 :)

Max Langhof
sumber
9

Kasus 1 [x](){} : Konstruktor yang dihasilkan akan menerima argumennya dengan constreferensi yang mungkin memenuhi syarat untuk menghindari salinan yang tidak perlu:

__some_compiler_generated_name(const int& x) : x_{x}{}

Kasus 2 [x&](){} : Asumsi Anda di sini benar, xditeruskan dan disimpan dengan referensi.


Kasus 3 [x = 33](){} : Sekali lagi benar, xdiinisialisasi oleh nilai.


Kasus 4 [p = std::move(unique_ptr_var)] : Konstruktor akan terlihat seperti ini:

    __some_compiler_generated_name(std::unique_ptr<SomeType>&& x) :
        x_{std::move(x)}{}

jadi ya, unique_ptr_varitu "pindah ke" penutupan. Lihat juga Item 32 Scott Meyer di C ++ Modern Efektif ("Gunakan tangkapan init untuk memindahkan objek ke penutupan").

lubgr
sumber
" const-Kualifikasi" Kenapa?
cpplearner
@ cpplearner Mh, pertanyaan bagus. Saya rasa saya memasukkan itu karena salah satu dari automatisme mental itu muncul ^^ Setidaknya consttidak ada salahnya di sini karena beberapa ambiguitas / kecocokan yang lebih baik ketika tidak constdll. Lagi pula, apakah Anda pikir saya harus menghapus const?
lubgr
Saya pikir const harus tetap, bagaimana, jika argumen yang diteruskan sebenarnya adalah const?
Aconcagua
Jadi Anda mengatakan bahwa konstruksi dua gerakan (atau salin) terjadi di sini?
Max Langhof
Maaf, maksud saya dalam kasus 4 (untuk bergerak) dan kasus 1 (untuk salinan). Bagian salinan pertanyaan saya tidak masuk akal berdasarkan pernyataan Anda (tetapi saya mempertanyakan pernyataan itu).
Max Langhof
5

Ada sedikit kebutuhan untuk berspekulasi, menggunakan cppinsights.io .

Kasus 1:
Kode

#include <memory>

int main() {
    int x = 33;
    auto lambda = [x]() { std::cout << x << std::endl; };
}

Kompiler menghasilkan

#include <iostream>

int main()
{
  int x = 6;

  class __lambda_5_16
  {
    int x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_5_16(const __lambda_5_16 &) = default;
    // inline /*constexpr */ __lambda_5_16(__lambda_5_16 &&) noexcept = default;
    public: __lambda_5_16(int _x)
    : x{_x}
    {}

  };

  __lambda_5_16 lambda = __lambda_5_16(__lambda_5_16{x});
}

Kasus 2:
Kode

#include <iostream>
#include <memory>

int main() {
    int x = 33;
    auto lambda = [&x]() { std::cout << x << std::endl; };
}

Kompiler menghasilkan

#include <iostream>

int main()
{
  int x = 6;

  class __lambda_5_16
  {
    int & x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_5_16(const __lambda_5_16 &) = default;
    // inline /*constexpr */ __lambda_5_16(__lambda_5_16 &&) noexcept = default;
    public: __lambda_5_16(int & _x)
    : x{_x}
    {}

  };

  __lambda_5_16 lambda = __lambda_5_16(__lambda_5_16{x});
}

Kasus 3:
Kode

#include <iostream>

int main() {
    auto lambda = [x = 33]() { std::cout << x << std::endl; };
}

Kompiler menghasilkan

#include <iostream>

int main()
{

  class __lambda_4_16
  {
    int x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_4_16(const __lambda_4_16 &) = default;
    // inline /*constexpr */ __lambda_4_16(__lambda_4_16 &&) noexcept = default;
    public: __lambda_4_16(int _x)
    : x{_x}
    {}

  };

  __lambda_4_16 lambda = __lambda_4_16(__lambda_4_16{33});
}

Kasus 4 (tidak resmi):
Kode

#include <iostream>
#include <memory>

int main() {
    auto x = std::make_unique<int>(33);
    auto lambda = [x = std::move(x)]() { std::cout << *x << std::endl; };
}

Kompiler menghasilkan

// EDITED output to minimize horizontal scrolling
#include <iostream>
#include <memory>

int main()
{
  std::unique_ptr<int, std::default_delete<int> > x = 
      std::unique_ptr<int, std::default_delete<int> >(std::make_unique<int>(33));

  class __lambda_6_16
  {
    std::unique_ptr<int, std::default_delete<int> > x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x.operator*()).operator<<(std::endl);
    }

    // inline __lambda_6_16(const __lambda_6_16 &) = delete;
    // inline __lambda_6_16(__lambda_6_16 &&) noexcept = default;
    public: __lambda_6_16(std::unique_ptr<int, std::default_delete<int> > _x)
    : x{_x}
    {}

  };

  __lambda_6_16 lambda = __lambda_6_16(__lambda_6_16{std::unique_ptr<int, 
                                                     std::default_delete<int> >
                                                         (std::move(x))});
}

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 bahwa operator()fungsinya. Tentu, jika Anda perlu memodifikasi tangkapan, Anda menandai lambda sebagai mutable.

sweenish
sumber
Kode yang Anda tunjukkan untuk case terakhir bahkan tidak dapat dikompilasi. Kesimpulan "suatu langkah terjadi, tetapi tidak [secara teknis] di konstruktor" tidak dapat didukung oleh kode itu.
Max Langhof
The Kode kasus 4 pasti tidak mengkompilasi pada Mac saya. Saya terkejut bahwa kode diperluas yang dihasilkan dari cppinsights tidak dapat dikompilasi. Situs ini, sampai saat ini, cukup dapat diandalkan untuk saya. Saya akan mengajukan masalah dengan mereka. EDIT: Saya mengkonfirmasi bahwa kode yang dihasilkan tidak dikompilasi; itu tidak jelas tanpa hasil edit ini.
sweenish
1
Tautkan ke masalah jika ada minat: github.com/andreasfertig/cppinsights/issues/258 Saya masih merekomendasikan situs untuk hal-hal seperti pengujian SFINAE dan apakah gips implisit akan terjadi atau tidak.
sweenish