Lambdas: dari C ++ 11 ke C ++ 20. Bagian 2

Hai, Habrovsk. Sehubungan dengan dimulainya rekrutmen ke dalam grup baru pada kursus “Pengembang C ++” , kami membagikan kepada Anda terjemahan dari bagian kedua artikel “Lambdas: dari C ++ 11 ke C ++ 20”. Bagian pertama bisa dibaca di sini .



Pada bagian pertama dari seri, kami melihat lambdas dalam hal C ++ 03, C ++ 11, dan C ++ 14. Dalam artikel ini, saya menggambarkan motivasi di balik fitur C ++ yang kuat ini, penggunaan dasar, sintaksis, dan peningkatan di setiap standar bahasa. Saya juga menyebutkan beberapa kasus perbatasan.
Sekarang saatnya untuk beralih ke C ++ 17 dan melihat masa depan (sangat dekat!): C ++ 20.

Entri

Pengingat cepat: ide untuk seri ini muncul setelah salah satu pertemuan Kelompok Pengguna C ++ kami baru-baru ini di Krakow.

Kami memiliki sesi pemrograman langsung tentang "sejarah" ekspresi lambda. Percakapan dilakukan oleh pakar C ++ Thomas Kaminsky ( lihat profil Thomas's Linkedin ). Inilah acaranya:
Lambdas: Dari C ++ 11 ke C ++ 20 - C ++ Grup Pengguna Krakow .

Saya memutuskan untuk mengambil kode dari Thomas (dengan izinnya!) Dan menulis artikel berdasarkan itu. Pada bagian pertama dari seri saya berbicara tentang ekspresi lambda sebagai berikut:

  • Sintaks dasar
  • Tipe lambda
  • Hubungi operator
  • Menangkap variabel (dapat diubah, global, variabel statis, anggota kelas dan penunjuk ini, objek yang hanya dapat bergerak, menyimpan konstanta):

    • Jenis kembali
    • IIFE - Ekspresi Fungsi Segera Diminta
    • Konversi ke penunjuk fungsi
    • Jenis kembali
    • IIFE - Ekspresi yang Segera Diminta
    • Konversi ke pointer fungsi
  • Perbaikan dalam C ++ 14

    • Jenis keluaran kembali
    • Abadikan dengan penginisialisasi
    • Tangkap variabel anggota
    • Ekspresi lambda generik

Daftar di atas hanya bagian dari sejarah ekspresi lambda!

Sekarang mari kita lihat apa yang telah berubah di C ++ 17 dan apa yang kita dapatkan di C ++ 20!

Perbaikan dalam C ++ 17

Standar (konsep sebelum publikasi) bagian N659 tentang lambdas: [expr.prim.lambda] . C ++ 17 membawa dua perbaikan signifikan pada ekspresi lambda:

  • constexpr lambda
  • Tangkap * ini

Apa arti inovasi ini bagi kita? Mari kita cari tahu.

ekspresi constexpr lambda

Dimulai dengan C ++ 17, standar secara implisit mendefinisikan operator() untuk tipe lambda sebagai constexpr , jika mungkin:
Dari expr.prim.lambda # 4 :
Operator panggilan fungsi adalah fungsi constexpr jika pernyataan parameter kondisi ekspresi lambda yang sesuai diikuti oleh constexpr, atau memenuhi persyaratan untuk fungsi constexpr.

Sebagai contoh:

 constexpr auto Square = [] (int n) { return n*n; }; // implicitly constexpr static_assert(Square(2) == 4); 

Ingatlah bahwa dalam C ++ 17 constexpr fungsi harus mengikuti aturan ini:

  • seharusnya tidak virtual;

    • tipe pengembaliannya harus tipe literal;
    • masing-masing tipe parameternya harus tipe literal;
    • tubuhnya harus = delete, = default atau pernyataan majemuk yang tidak mengandung
      • definisi asm
      • ekspresi kebotakan,
      • tag
      • coba blokir atau
      • definisi variabel non-literal, variabel statis, atau variabel memori streaming yang inisialisasi tidak dilakukan.

Bagaimana dengan contoh yang lebih praktis?

 template<typename Range, typename Func, typename T> constexpr T SimpleAccumulate(const Range& range, Func func, T init) { for (auto &&elem: range) { init += func(elem); } return init; } int main() { constexpr std::array arr{ 1, 2, 3 }; static_assert(SimpleAccumulate(arr, [](int i) { return i * i; }, 0) == 14); } 

Anda dapat bermain dengan kode di sini: @ Wandbox

Kode menggunakan lambda constexpr , dan kemudian diteruskan ke algoritma SimpleAccumulate sederhana. Algoritme menggunakan beberapa elemen C ++ 17: penambahan constexpr ke std::array , std::begin dan std::end (digunakan dalam for loop dengan range) sekarang juga constexpr , jadi ini berarti semua kode dapat dieksekusi pada waktu kompilasi.

Tentu saja tidak semuanya.

Anda dapat menangkap variabel (asalkan mereka juga constexpr ):

 constexpr int add(int const& t, int const& u) { return t + u; } int main() { constexpr int x = 0; constexpr auto lam = [x](int n) { return add(x, n); }; static_assert(lam(10) == 10); } 

Tetapi ada kasus menarik ketika Anda tidak meneruskan variabel yang ditangkap lebih lanjut, misalnya:

 constexpr int x = 0; constexpr auto lam = [x](int n) { return n + x }; 

Dalam hal ini, di Dentang kita bisa mendapatkan peringatan berikut:

warning: lambda capture 'x' is not required to be captured for this use

Ini mungkin karena fakta bahwa x dapat diubah di tempat dengan setiap penggunaan (kecuali jika Anda mentransfernya lebih lanjut atau mengambil alamat nama ini).

Tapi tolong beri tahu saya jika Anda tahu aturan resmi untuk perilaku ini. Saya hanya menemukan (dari cppreference ) (tapi saya tidak dapat menemukannya di draft ...)

(Catatan Penerjemah: ketika pembaca kami menulis, maksud saya mungkin mengganti nilai 'x' di setiap tempat di mana ia digunakan. Jelas tidak mungkin untuk mengubahnya.)

Ekspresi lambda dapat membaca nilai variabel tanpa menangkapnya jika variabel
* memiliki integer non-volatile konstan atau tipe enumerated dan telah diinisialisasi dengan constexpr atau
* adalah constexpr dan tidak memiliki anggota yang bisa berubah.

Bersiaplah untuk masa depan:

Dalam C ++ 20, kita akan memiliki algoritma standar constexpr dan, mungkin, bahkan beberapa kontainer, jadi constexpr akan sangat berguna dalam konteks ini. Kode Anda akan terlihat sama untuk versi run-time dan juga untuk versi constexpr (versi waktu kompilasi)!

Singkatnya:

constexpr lambda memungkinkan Anda untuk konsisten dengan pemrograman boilerplate dan mungkin memiliki kode yang lebih pendek.

Sekarang mari kita beralih ke fitur penting kedua yang tersedia di C ++ 17:

Tangkapan * ini
Tangkap * ini

Apakah Anda ingat masalah kami ketika kami ingin menangkap anggota kelas? Secara default, kami menangkap ini (sebagai penunjuk!), Dan karena itu kami mungkin memiliki masalah ketika objek sementara keluar dari ruang lingkup ... Ini dapat diperbaiki menggunakan metode penangkapan dengan penginisialisasi (lihat bagian pertama dari seri). Tetapi sekarang, di C ++ 17, kami memiliki cara yang berbeda. Kami dapat membungkus salinan * ini:

 #include <iostream> struct Baz { auto foo() { return [*this] { std::cout << s << std::endl; }; } std::string s; }; int main() { auto f1 = Baz{"ala"}.foo(); auto f2 = Baz{"ula"}.foo(); f1(); f2(); } 

Anda dapat bermain dengan kode di sini: @ Wandbox

Menangkap variabel anggota yang diinginkan menggunakan tangkapan dengan penginisialisasi melindungi Anda dari kemungkinan kesalahan dengan nilai sementara, tetapi kami tidak dapat melakukan hal yang sama ketika kami ingin memanggil metode seperti:

Sebagai contoh:

 struct Baz { auto foo() { return [this] { print(); }; } void print() const { std::cout << s << '\n'; } std::string s; }; 

Di C ++ 14, satu-satunya cara untuk membuat kode lebih aman adalah dengan menangkap this dengan initializer:

 auto foo() { return [self=*this] { self.print(); }; }   C ++ 17    : auto foo() { return [*this] { print(); }; } 

Satu hal lagi:

Perhatikan bahwa jika Anda menulis [=] dalam fungsi anggota, this ditangkap secara implisit! Ini dapat menyebabkan kesalahan di masa depan ... dan itu akan menjadi usang di C ++ 20.

Jadi kita sampai pada bagian selanjutnya: masa depan.

Masa depan dengan C ++ 20

Di C ++ 20, kita mendapatkan fungsi-fungsi berikut:

  • Izinkan [=, this] sebagai tangkapan lambda - P0409R2 dan batalkan penangkapan implisit ini melalui [=] - P0806
  • Ekstensi paket dalam lambda init-capture: ... args = std::move (args)] () {} - P0780
  • thread_local statis, thread_local dan lambda untuk binding terstruktur - P1091
  • pola lambda (juga dengan konsep) - P0428R2
  • Penyederhanaan Lambda Capture - P0588R1
  • Lambda Konstruktif dan ditugaskan tanpa menyimpan keadaan default - P0624R2
  • Lambdas dalam konteks yang tidak dihitung - P0315R4

Dalam kebanyakan kasus, fungsi yang baru diperkenalkan "membersihkan" penggunaan lambda, dan mereka memungkinkan untuk beberapa kasus penggunaan lanjutan.

Misalnya, dengan P1091 Anda dapat menangkap ikatan terstruktur.

Kami juga memiliki klarifikasi terkait dengan menangkap ini. Di C ++ 20, Anda akan mendapatkan peringatan jika Anda menangkap [=] dalam metode:

 struct Baz { auto foo() { return [=] { std::cout << s << std::endl; }; } std::string s; }; GCC 9: warning: implicit capture of 'this' via '[=]' is deprecated in C++20 

Jika Anda benar-benar perlu menangkap ini, Anda harus menulis [=, this] .

Ada juga perubahan yang terkait dengan kasus penggunaan tingkat lanjut, seperti konteks tanpa kewarganegaraan dan lambdas tanpa kewarganegaraan yang dapat dibangun secara default.

Dengan kedua perubahan, Anda dapat menulis:

 std::map<int, int, decltype([](int x, int y) { return x > y; })> map; 

Baca motif untuk fitur-fitur ini dalam versi pertama kalimat: P0315R0 dan P0624R0 .

Tapi mari kita lihat satu fitur menarik: templat lambda.

Pola lambd

Di C ++ 14, kami mendapatkan lambdas umum, yang berarti bahwa parameter yang dideklarasikan sebagai otomatis adalah parameter templat.

Untuk lambda:

 [](auto x) { x; } 

Kompiler menghasilkan pernyataan panggilan yang cocok dengan metode boilerplate berikut:

 template<typename T> void operator(T x) { x; } 

Tetapi tidak ada cara untuk mengubah parameter template ini dan menggunakan argumen template yang sebenarnya. Dalam C ++ 20, ini akan dimungkinkan.

Misalnya, bagaimana kita dapat membatasi lambda kita hanya untuk bekerja dengan vektor dari beberapa jenis?

Kita dapat menulis lambda umum:

 auto foo = []<typename T>(const auto& vec) { std::cout<< std::size(vec) << '\n'; std::cout<< vec.capacity() << '\n'; }; 

Tetapi jika Anda menyebutnya dengan parameter int (misalnya, foo(10); ), Anda mungkin mendapatkan beberapa kesalahan yang sulit dibaca:

 prog.cc: In instantiation of 'main()::<lambda(const auto:1&)> [with auto:1 = int]': prog.cc:16:11: required from here prog.cc:11:30: error: no matching function for call to 'size(const int&)' 11 | std::cout<< std::size(vec) << '\n'; 

Dalam C ++ 20 kita dapat menulis:

 auto foo = []<typename T>(std::vector<T> const& vec) { std::cout<< std::size(vec) << '\n'; std::cout<< vec.capacity() << '\n'; }; 

Lambda di atas memungkinkan pernyataan panggilan templat:

 <typename T> void operator(std::vector<T> const& s) { ... } 

Parameter templat mengikuti klausa tangkap [] .

Jika Anda menyebutnya dengan int (foo(10);) , Anda akan mendapatkan pesan yang lebih bagus:

 note: mismatched types 'const std::vector<T>' and 'int' 


Anda dapat bermain dengan kode di sini: @ Wandbox

Dalam contoh di atas, kompiler dapat memperingatkan kita tentang ketidakkonsistenan dalam antarmuka lambda daripada dalam kode di dalam tubuh.

Aspek penting lainnya adalah bahwa dalam lambda universal Anda hanya memiliki variabel, bukan tipe templatnya. Oleh karena itu, jika Anda ingin mengaksesnya, Anda harus menggunakan decltype (x) (untuk ekspresi lambda dengan argumen (auto x)). Ini membuat beberapa kode lebih verbose dan rumit.

Misalnya (menggunakan kode dari P0428):

 auto f = [](auto const& x) { using T = std::decay_t<decltype(x)>; T copy = x; T::static_function(); using Iterator = typename T::iterator; } 

Sekarang Anda dapat menulis sebagai:

 auto f = []<typename T>(T const& x) { T::static_function(); T copy = x; using Iterator = typename T::iterator; } 

Pada bagian di atas, kami memiliki gambaran umum singkat tentang C ++ 20, tetapi saya memiliki case use tambahan untuk Anda. Teknik ini bahkan mungkin di C ++ 14. Jadi baca terus.

Bonus - LIFTing dengan lambdas

Saat ini kami memiliki masalah ketika Anda memiliki kelebihan fungsi dan Anda ingin meneruskannya ke algoritma standar (atau apa pun yang memerlukan beberapa objek yang disebut):

 // two overloads: void foo(int) {} void foo(float) {} int main() { std::vector<int> vi; std::for_each(vi.begin(), vi.end(), foo); } 

Kami mendapatkan kesalahan berikut dari GCC 9 (trunk):

 error: no matching function for call to for_each(std::vector<int>::iterator, std::vector<int>::iterator, <unresolved overloaded function type>) std::for_each(vi.begin(), vi.end(), foo); ^^^^^ 

Namun, ada trik di mana kita dapat menggunakan lambda dan kemudian memanggil fungsi kelebihan yang diinginkan.

Dalam bentuk dasar, untuk tipe nilai sederhana, untuk dua fungsi kami, kami dapat menulis kode berikut:

 std::for_each(vi.begin(), vi.end(), [](auto x) { return foo(x); }); 

Dan dalam bentuk yang paling umum, kita perlu mengetik lebih banyak:

 #define LIFT(foo) \ [](auto&&... x) \ noexcept(noexcept(foo(std::forward<decltype(x)>(x)...))) \ -> decltype(foo(std::forward<decltype(x)>(x)...)) \ { return foo(std::forward<decltype(x)>(x)...); } 

Kode yang cukup rumit ... bukan? :)

Mari kita coba mendekripsi:

Kami membuat lambda generik dan kemudian meneruskan semua argumen yang kami dapatkan. Untuk menentukannya dengan benar, kita perlu menentukan noexcept dan jenis nilai pengembalian. Itu sebabnya kami harus menduplikasi kode panggilan - untuk mendapatkan tipe yang benar.
Makro LIFT seperti itu berfungsi di kompiler apa pun yang mendukung C ++ 14.

Anda dapat bermain dengan kode di sini: @ Wandbox

Kesimpulan

Dalam posting ini, kami melihat perubahan signifikan dalam C ++ 17, dan memberikan gambaran umum fitur-fitur baru di C ++ 20.

Anda mungkin memperhatikan bahwa dengan setiap iterasi bahasa, ekspresi lambda bercampur dengan elemen C ++ lainnya. Sebagai contoh, sebelum C ++ 17, kita tidak bisa menggunakannya dalam konteks constexpr, tetapi sekarang dimungkinkan. Demikian pula dengan lambdas generik yang dimulai dengan C ++ 14 dan evolusinya menjadi C ++ 20 dalam bentuk templat lambdas. Apakah saya melewatkan sesuatu? Mungkin Anda punya contoh menarik? Tolong beri tahu saya di komentar!

Referensi

C ++ 11 - [expr.prim.lambda]
C ++ 14 - [expr.prim.lambda]
C ++ 17 - [expr.prim.lambda]
Ekspresi Lambda di C ++ | Dokumen Microsoft
Simon Brand - Passing overload set ke fungsi
Jason Turner - C ++ Weekly - Ep 128 - C ++ 20's Sintaks Templat Untuk Lambdas
Jason Turner - C ++ Mingguan - Ep 41 - C ++ 17's Dukungan Lambda constexpr

Kami mengundang semua orang ke webinar tradisional gratis pada kursus, yang akan berlangsung besok 14 Juni.

Source: https://habr.com/ru/post/id455978/


All Articles