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

Selamat siang teman Hari ini kami telah menyiapkan untuk Anda terjemahan dari bagian pertama artikel “Lambdas: from C ++ 11 to C ++ 20” . Publikasi materi ini dijadwalkan bertepatan dengan peluncuran kursus "Pengembang C ++" , yang dimulai besok.

Ekspresi Lambda adalah salah satu tambahan paling kuat di C ++ 11 dan terus berkembang dengan setiap standar bahasa baru. Dalam artikel ini, kita akan membahas sejarah mereka dan melihat evolusi bagian penting dari C ++ modern ini.



Bagian kedua tersedia di sini:
Lambdas: Dari C ++ 11 hingga C ++ 20, Bagian 2

Entri

Pada pertemuan Kelompok Pengguna C ++ lokal, kami mengadakan sesi pemrograman langsung tentang "sejarah" ekspresi lambda. Percakapan ini dipimpin oleh pakar C ++ Tomasz Kamiński ( 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!), Jelaskan dan buat artikel terpisah.

Kami akan mulai dengan menjelajahi C ++ 03 dan kebutuhan untuk ekspresi fungsional lokal yang kompak. Kemudian kita beralih ke C ++ 11 dan C ++ 14. Di bagian kedua seri ini, kita akan melihat perubahan dalam C ++ 17 dan bahkan melihat apa yang akan terjadi di C ++ 20.

Lambdas di C ++ 03

Sejak awal, std::algorithms STL, seperti std::sort , dapat mengambil objek apa pun yang disebut dan menyebutnya pada elemen kontainer. Namun, dalam C ++ 03, ini hanya melibatkan pointer ke fungsi dan fungsi.

Sebagai contoh:

 #include <iostream> #include <algorithm> #include <vector> struct PrintFunctor { void operator()(int x) const { std::cout << x << std::endl; } }; int main() { std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), PrintFunctor()); } 

Kode yang sedang dijalankan : @Wandbox

Tetapi masalahnya adalah bahwa Anda harus menulis fungsi atau functor terpisah dalam lingkup yang berbeda, dan bukan dalam lingkup panggilan algoritma.

Sebagai solusi potensial, Anda dapat mempertimbangkan untuk menulis kelas functor lokal - karena C ++ selalu mendukung sintaks ini. Tapi itu tidak berhasil ...

Lihatlah kode ini:

 int main() { struct PrintFunctor { void operator()(int x) const { std::cout << x << std::endl; } }; std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), PrintFunctor()); } 

Coba kompilasi dengan -std=c++98 dan Anda akan melihat kesalahan berikut di GCC:

 error: template argument for 'template<class _IIter, class _Funct> _Funct std::for_each(_IIter, _IIter, _Funct)' uses local type 'main()::PrintFunctor' 

Pada dasarnya, di C ++ 98/03, Anda tidak bisa membuat instance templat dengan tipe lokal.
Karena semua keterbatasan ini, Komite mulai mengembangkan fitur baru yang dapat kita buat dan sebut "di tempat" ... "ekspresi lambda"!

Jika kita melihat N3337 - versi final C ++ 11, kita akan melihat bagian terpisah untuk lambdas: [expr.prim.lambda] .

Di sebelah C ++ 11

Saya pikir lambdas telah ditambahkan ke bahasa dengan bijak. Mereka menggunakan sintaks baru, tetapi kemudian kompiler "meluas" ke kelas nyata. Dengan demikian, kami memiliki semua kelebihan (dan terkadang kelemahan) dari bahasa yang diketik dengan benar.

Berikut ini adalah contoh kode dasar yang juga memperlihatkan objek functor lokal yang sesuai:

 #include <iostream> #include <algorithm> #include <vector> int main() { struct { void operator()(int x) const { std::cout << x << '\n'; } } someInstance; std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), someInstance); std::for_each(v.begin(), v.end(), [] (int x) { std::cout << x << '\n'; } ); } 

Contoh: @WandBox

Anda juga dapat melihat CppInsights, yang menunjukkan bagaimana kompiler memperluas kode:

Lihatlah contoh ini:

CppInsighs: tes lambda

Dalam contoh ini, kompiler mengkonversi:

 [] (int x) { std::cout << x << '\n'; } 


Ke dalam sesuatu yang mirip dengan ini (bentuk disederhanakan):

 struct { void operator()(int x) const { std::cout << x << '\n'; } } someInstance; 

Sintaks ekspresi Lambda:

 [] () { ; } ^ ^ ^ | | | | | : mutable, exception, trailing return, ... | | |   |      

Beberapa definisi sebelum kita mulai:

Dari [expr.prim.lambda # 2] :

Mengevaluasi ekspresi lambda menghasilkan nilai sementara. Objek sementara ini disebut objek penutupan .

Dan dari [expr.prim.lambda # 3] :

Tipe ekspresi lambda (yang juga merupakan tipe dari objek penutupan) adalah tipe non-union tanpa nama yang unik dari kelas yang disebut tipe closure .

Beberapa contoh ekspresi lambda:

Sebagai contoh:

 [](float f, int a) { return a*f; } [](MyClass t) -> int { auto a = t.compute(); return a; } [](int a, int b) { return a < b; } 

Tipe lambda

Karena kompiler menghasilkan nama unik untuk setiap lambda, tidak mungkin untuk mengetahuinya terlebih dahulu.

 auto myLambda = [](int a) -> double { return 2.0 * a; } 

Selain itu [expr.prim.lambda] :
Jenis penutupan yang terkait dengan ekspresi lambda memiliki konstruktor default jarak jauh ([dcl.fct.def.delete]) dan operator penetapan jarak jauh.

Karenanya, Anda tidak dapat menulis:

 auto foo = [&x, &y]() { ++x; ++y; }; decltype(foo) fooCopy; 

Ini akan menghasilkan kesalahan berikut dalam GCC:

 error: use of deleted function 'main()::<lambda()>::<lambda>()' decltype(foo) fooCopy; ^~~~~~~ note: a lambda closure type has a deleted default constructor 

Hubungi operator

Kode yang Anda masukkan ke dalam tubuh lambda "diterjemahkan" ke dalam kode operator () dari tipe penutupan yang sesuai.

Secara default, ini adalah metode konstan bawaan. Anda dapat mengubahnya dengan menentukan bisa berubah setelah mendeklarasikan parameter:

 auto myLambda = [](int a) mutable { std::cout << a; } 

Meskipun metode konstan bukan merupakan "masalah" untuk lambda tanpa daftar tangkap kosong ... itu penting ketika Anda ingin menangkap sesuatu.

Tangkap

[] tidak hanya memperkenalkan lambda, tetapi juga berisi daftar variabel yang ditangkap. Ini disebut daftar tangkap.

Dengan menangkap variabel, Anda membuat salinan anggota variabel ini dalam tipe penutupan. Kemudian di dalam tubuh lambda Anda dapat mengaksesnya.

Sintaks dasarnya adalah:

  • [&] - tangkap dengan referensi, semua variabel dalam penyimpanan otomatis dinyatakan dalam cakupan
  • [=] - tangkap menurut nilai, nilainya disalin
  • [x, & y] - secara eksplisit menangkap x berdasarkan nilai, dan y dengan referensi

Sebagai contoh:

 int x = 1, y = 1; { std::cout << x << " " << y << std::endl; auto foo = [&x, &y]() { ++x; ++y; }; foo(); std::cout << x << " " << y << std::endl; } 

Anda dapat bermain-main dengan contoh lengkapnya di sini: @Wandbox

Meskipun menentukan [=] atau [&] dapat dilakukan - karena ia menangkap semua variabel dalam penyimpanan otomatis, lebih jelas untuk menangkap variabel secara eksplisit. Dengan demikian, kompiler dapat memperingatkan Anda tentang efek yang tidak diinginkan (lihat, misalnya, catatan tentang variabel global dan statis)

Anda juga dapat membaca lebih lanjut dalam paragraf 31 dari Efektif Modern C ++ oleh Scott Meyers: "Hindari mode penangkapan default."

Dan kutipan penting:
Penutupan C ++ tidak meningkatkan masa pakai tautan yang diambil.


Bisa berubah

Secara default, operator tipe penutupan () adalah konstan, dan Anda tidak dapat mengubah variabel yang ditangkap di dalam tubuh ekspresi lambda.
Jika Anda ingin mengubah perilaku ini, Anda perlu menambahkan kata kunci yang bisa berubah setelah daftar parameter:

 int x = 1, y = 1; std::cout << x << " " << y << std::endl; auto foo = [x, y]() mutable { ++x; ++y; }; foo(); std::cout << x << " " << y << std::endl; 

Pada contoh di atas, kita dapat mengubah nilai x dan y ... tetapi ini hanya salinan x dan y dari cakupan terlampir.

Pengambilan variabel global

Jika Anda memiliki nilai global, dan kemudian Anda menggunakan [=] dalam lambda, Anda mungkin berpikir bahwa nilai global juga ditangkap oleh nilai ... tetapi tidak.

 int global = 10; int main() { std::cout << global << std::endl; auto foo = [=] () mutable { ++global; }; foo(); std::cout << global << std::endl; [] { ++global; } (); std::cout << global << std::endl; [global] { ++global; } (); } 

Anda dapat bermain dengan kode di sini: @ Wandbox

Hanya variabel dalam penyimpanan otomatis yang ditangkap. GCC bahkan dapat mengeluarkan peringatan berikut:

 warning: capture of variable 'global' with non-automatic storage duration 

Peringatan ini hanya akan muncul jika Anda secara eksplisit menangkap variabel global, jadi jika Anda menggunakan [=] , kompiler tidak akan membantu Anda.
Compiler Dentang lebih berguna karena menghasilkan kesalahan:

 error: 'global' cannot be captured because it does not have automatic storage duration 

Lihat @ Kotak kotak

Menangkap Variabel Statis

Menangkap variabel statis mirip dengan menangkap global:

 #include <iostream> void bar() { static int static_int = 10; std::cout << static_int << std::endl; auto foo = [=] () mutable { ++static_int; }; foo(); std::cout << static_int << std::endl; [] { ++static_int; } (); std::cout << static_int << std::endl; [static_int] { ++static_int; } (); } int main() { bar(); } 

Anda dapat bermain dengan kode di sini: @ Wandbox

Kesimpulan:

 10 11 12 

Dan lagi, peringatan hanya akan muncul jika Anda secara eksplisit menangkap variabel statis, jadi jika Anda menggunakan [=] , kompiler tidak akan membantu Anda.

Tangkapan Anggota Kelas

Apakah Anda tahu apa yang terjadi setelah mengeksekusi kode berikut:

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

Kode menyatakan objek Baz dan kemudian memanggil foo() . Perhatikan bahwa foo() mengembalikan lambda (disimpan di std::function ) yang menangkap anggota kelas.

Karena kita menggunakan objek sementara, kita tidak bisa memastikan apa yang akan terjadi ketika f1 dan f2 dipanggil. Ini adalah masalah tautan menggantung yang menyebabkan perilaku tidak terdefinisi.

Demikian pula:

 struct Bar { std::string const& foo() const { return s; }; std::string s; }; auto&& f1 = Bar{"ala"}.foo(); //   

Mainkan dengan kode @Wandbox

Sekali lagi, jika Anda menentukan tangkapan secara eksplisit:

 std::function<void()> foo() { return [s] { std::cout << s << std::endl; }; } 

Kompiler akan mencegah kesalahan Anda:

 In member function 'std::function<void()> Baz::foo()': error: capture of non-variable 'Baz::s' error: 'this' was not captured for this lambda function ... 

Lihat sebuah contoh: @Wandbox

Benda yang hanya dapat bergerak

Jika Anda memiliki objek yang hanya bisa dipindahkan (misalnya, unique_ptr), maka Anda tidak bisa memasukkannya ke dalam lambda sebagai variabel yang diambil. Menangkap berdasarkan nilai tidak berfungsi, jadi Anda hanya dapat menangkap dengan referensi ... namun, ini tidak akan mentransfernya kepada Anda, dan mungkin ini bukan yang Anda inginkan.

 std::unique_ptr<int> p(new int[10]); auto foo = [p] () {}; //  .... 

Menyimpan Konstanta

Jika Anda menangkap variabel konstan, maka konstanta dipertahankan:

 int const x = 10; auto foo = [x] () mutable { std::cout << std::is_const<decltype(x)>::value << std::endl; x = 11; }; foo(); 

Lihat kode: @Wandbox

Jenis kembali

Di C ++ 11, Anda dapat melewati trailing tipe lambda yang kembali, dan kemudian kompiler akan menampilkannya untuk Anda.

Awalnya, output dari tipe nilai pengembalian terbatas pada lambdas yang mengandung satu pernyataan pengembalian, tetapi pembatasan ini dengan cepat dihapus, karena tidak ada masalah dengan implementasi versi yang lebih nyaman.

Lihat Laporan C ++ Standar Inti Bahasa C ++ dan Masalah yang Diterima (terima kasih kepada Thomas untuk menemukan tautan yang tepat!)

Jadi, dimulai dengan C ++ 11, kompiler dapat menyimpulkan tipe nilai kembali jika semua pernyataan kembali dapat dikonversi ke tipe yang sama.
Jika semua pernyataan pengembalian mengembalikan ekspresi dan jenis kembali setelah konversi lvalue-to-rvalue (7.1 [conv.lval]), array-to-pointer (7.2 [conv.array]) dan function-to-pointer (7.3 [conv. func]) sama dengan tipe generik;
 auto baz = [] () { int x = 10; if ( x < 20) return x * 1.1; else return x * 2.1; }; 

Anda dapat bermain dengan kode di sini: @ Wandbox

Ada dua return dalam lambda di atas, tetapi semuanya menunjuk ke double , sehingga kompiler dapat menyimpulkan jenisnya.

IIFE - Ekspresi Fungsi Segera Diminta

Dalam contoh kami, saya mendefinisikan lambda dan kemudian menyebutnya menggunakan objek penutupan ... tetapi juga dapat dipanggil segera:

 int x = 1, y = 1; [&]() { ++x; ++y; }(); // <-- call () std::cout << x << " " << y << std::endl; 

Ungkapan seperti itu dapat berguna dalam inisialisasi kompleks objek konstan.

 const auto val = []() { /*   ... */ }(); 

Saya menulis lebih banyak tentang ini di pos IIFE untuk Inisialisasi Kompleks .

Konversi ke pointer fungsi
Tipe closure untuk ekspresi lambda tanpa capture memiliki fungsi implisit non-virtual terbuka untuk mengubah konstanta menjadi pointer ke fungsi yang memiliki parameter yang sama dan mengembalikan tipe ketika operator memanggil fungsi tipe closure. Nilai yang dikembalikan oleh fungsi konversi ini harus alamat fungsi, yang ketika dipanggil memiliki efek yang sama dengan memanggil operator dari fungsi yang mirip dengan tipe penutupan.
Dengan kata lain, Anda dapat mengonversi lambdas tanpa menangkap ke pointer fungsi.

Sebagai contoh:

 #include <iostream> void callWith10(void(* bar)(int)) { bar(10); } int main() { struct { using f_ptr = void(*)(int); void operator()(int s) const { return call(s); } operator f_ptr() const { return &call; } private: static void call(int s) { std::cout << s << std::endl; }; } baz; callWith10(baz); callWith10([](int x) { std::cout << x << std::endl; }); } 

Anda dapat bermain dengan kode di sini: @ Wandbox

Perbaikan dalam C ++ 14

N4140 standar dan lambda: [expr.prim.lambda] .

C ++ 14 menambahkan dua peningkatan signifikan pada ekspresi lambda:

  • Diambil dengan Initializer
  • Lambda umum

Fitur-fitur ini memecahkan beberapa masalah yang terlihat di C ++ 11.

Jenis kembali

Output dari tipe nilai pengembalian ekspresi lambda telah diperbarui untuk mematuhi aturan output otomatis untuk fungsi.

[expr.prim.lambda # 4]
Tipe kembalinya lambda adalah otomatis, yang digantikan oleh tipe trailing return, jika disediakan dan / atau disimpulkan dari pernyataan pengembalian, seperti dijelaskan dalam [dcl.spec.auto].
Diambil dengan Initializer

Singkatnya, kita bisa membuat variabel anggota baru dari tipe penutupan dan kemudian menggunakannya di dalam ekspresi lambda.

Sebagai contoh:

 int main() { int x = 10; int y = 11; auto foo = [z = x+y]() { std::cout << z << '\n'; }; foo(); } 

Ini dapat memecahkan beberapa masalah, misalnya, dengan tipe yang hanya tersedia untuk bergerak.

Bergerak

Sekarang kita bisa memindahkan objek ke anggota tipe penutupan:

 #include <memory> int main() { std::unique_ptr<int> p(new int[10]); auto foo = [x=10] () mutable { ++x; }; auto bar = [ptr=std::move(p)] {}; auto baz = [p=std::move(p)] {}; } 

Optimasi

Gagasan lain adalah menggunakannya sebagai teknik optimasi potensial. Alih-alih menghitung beberapa nilai setiap kali kita memanggil lambda, kita dapat menghitungnya sekali di inisialisasi:

 #include <iostream> #include <algorithm> #include <vector> #include <memory> #include <iostream> #include <string> int main() { using namespace std::string_literals; std::vector<std::string> vs; std::find_if(vs.begin(), vs.end(), [](std::string const& s) { return s == "foo"s + "bar"s; }); std::find_if(vs.begin(), vs.end(), [p="foo"s + "bar"s](std::string const& s) { return s == p; }); } 

Tangkap variabel anggota

Inisialisasi juga dapat digunakan untuk menangkap variabel anggota. Kemudian kita bisa mendapatkan salinan variabel anggota dan tidak khawatir tentang tautan yang menggantung.

Sebagai contoh:

 struct Baz { auto foo() { return [s=s] { 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


Di foo() kami menangkap variabel anggota dengan menyalinnya ke tipe penutupan. Selain itu, kami menggunakan otomatis untuk menampilkan seluruh metode (sebelumnya, dalam C ++ 11 kami dapat menggunakan std::function ).

Ekspresi lambda generik

Peningkatan signifikan lainnya adalah lambda umum.
Dimulai dengan C ++ 14 Anda dapat menulis:

 auto foo = [](auto x) { std::cout << x << '\n'; }; foo(10); foo(10.1234); foo("hello world"); 

Ini sama dengan menggunakan deklarasi templat dalam pernyataan panggilan jenis penutupan:

 struct { template<typename T> void operator()(T x) const { std::cout << x << '\n'; } } someInstance; 

Lambda umum seperti itu bisa sangat berguna ketika sulit untuk menyimpulkan suatu jenis.

Sebagai contoh:

 std::map<std::string, int> numbers { { "one", 1 }, {"two", 2 }, { "three", 3 } }; //      pair<const string, int>! std::for_each(std::begin(numbers), std::end(numbers), [](const std::pair<std::string, int>& entry) { std::cout << entry.first << " = " << entry.second << '\n'; } ); 

Apakah saya salah di sini? Apakah entri memiliki tipe yang benar?
.
.
.
Mungkin tidak, karena tipe nilai untuk std :: map adalah std::pair<const Key, T> . Jadi kode saya akan membuat salinan tambahan dari baris ...
Ini dapat diperbaiki dengan auto :

 std::for_each(std::begin(numbers), std::end(numbers), [](auto& entry) { std::cout << entry.first << " = " << entry.second << '\n'; } ); 

Anda dapat bermain dengan kode di sini: @ Wandbox

Kesimpulan

Cerita yang luar biasa!

Pada artikel ini, kami mulai dari hari pertama ekspresi lambda di C ++ 03 dan C ++ 11 dan beralih ke versi yang lebih baik di C ++ 14.

Anda melihat cara membuat lambda, apa struktur dasar ungkapan ini, apa itu daftar tangkap, dan banyak lagi.

Pada bagian selanjutnya dari artikel ini, kita akan beralih ke C ++ 17 dan mengenal fitur C ++ 20 di masa depan.

Bagian kedua tersedia di sini:

Lambdas: Dari C ++ 11 hingga C ++ 20, Bagian 2


Referensi

C ++ 11 - [expr.prim.lambda]
C ++ 14 - [expr.prim.lambda]
Ekspresi Lambda di C ++ | Dokumen Microsoft
Demystifying C ++ lambdas - Bit Sticky - Didukung oleh Feabhas; Bit Sticky - Didukung oleh Feabhas


Kami menunggu komentar Anda dan mengundang semua orang yang tertarik dengan kursus "Pengembang C ++" .

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


All Articles