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.
EntriPengingat 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 ++ 17Standar (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 lambdaDimulai 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:
@ WandboxKode 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 * iniTangkap * iniApakah 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:
Anda dapat bermain dengan kode di sini:
@ WandboxMenangkap 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 ++ 20Di 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 lambdDi 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:
@ WandboxDalam 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 lambdasSaat 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:
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:
@ WandboxKesimpulanDalam 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!
ReferensiC ++ 11 -
[expr.prim.lambda]C ++ 14 -
[expr.prim.lambda]C ++ 17 -
[expr.prim.lambda]Ekspresi Lambda di C ++ | Dokumen MicrosoftSimon Brand -
Passing overload set ke fungsiJason Turner -
C ++ Weekly - Ep 128 - C ++ 20's Sintaks Templat Untuk LambdasJason Turner -
C ++ Mingguan - Ep 41 - C ++ 17's Dukungan Lambda constexprKami mengundang semua orang ke
webinar tradisional
gratis pada kursus, yang akan berlangsung besok 14 Juni.