Ekspresi Lambda di C++

Di C++11 dan yang lebih baru, ekspresi lambda—sering disebut lambda—adalah cara mudah untuk menentukan objek fungsi anonim ( penutupan) tepat di lokasi tempat objek tersebut dipanggil atau diteruskan sebagai argumen ke fungsi. Biasanya lambda digunakan untuk merangkum beberapa baris kode yang diteruskan ke algoritma atau fungsi asinkron. Artikel ini mendefinisikan apa itu lambda, dan membandingkannya dengan teknik pemrograman lainnya. Ini menjelaskan keuntungan mereka, dan memberikan beberapa contoh dasar.

Bagian dari ekspresi lambda

Berikut adalah lambda sederhana yang diteruskan sebagai argumen ketiga untuk std::sort() fungsi:

#include <algorithm>
#include <cmath>

void abssort(float* x, unsigned n) {
    std::sort(x, x + n,
        // Lambda expression begins
        [](float a, float b) {
            return (std::abs(a) < std::abs(b));
        } // end of lambda expression
    );
}

Ilustrasi ini menunjukkan bagian-bagian sintaks lambda:

Diagram that identifies the various parts of a lambda expression.

Contoh ekspresi lambda adalah [=]() mutable throw() -> int { return x+y; } [=] adalah klausa pengambilan; juga dikenal sebagai pengantar lambda dalam spesifikasi C++. Tanda kurung adalah untuk daftar parameter. Kata kunci yang dapat diubah bersifat opsional. throw() adalah spesifikasi pengecualian opsional. -> int adalah jenis pengembalian berikutnya opsional. Tubuh lambda terdiri dari pernyataan di dalam kurung kurawal, atau mengembalikan x+y; Ini dijelaskan secara lebih rinci setelah gambar.

  1. mengambil klausul (Juga dikenal sebagai pengantar lambda dalam spesifikasi C++.)

  2. daftar parameter Opsional. (Juga dikenal sebagai deklarator lambda)

  3. spesifikasi yang dapat diubah Opsional.

  4. spesifikasi pengecualian Opsional.

  5. Opsional trailing-return-type .

  6. tubuh lambda.

Mengambil klausa

Lambda dapat memperkenalkan variabel baru dalam tubuhnya (dalam C++14), dan juga dapat mengakses, atau menangkap, variabel dari cakupan sekitarnya. Lambda dimulai dengan klausul pengambilan. Ini menentukan variabel mana yang diambil, dan apakah tangkapan berdasarkan nilai atau berdasarkan referensi. Variabel yang memiliki awalan ampersand (&) diakses oleh referensi dan variabel yang tidak memilikinya diakses oleh nilai.

Klausa tangkapan kosong, [ ], menunjukkan bahwa isi ekspresi lambda tidak mengakses variabel dalam cakupan penutup.

Anda dapat menggunakan mode capture-default untuk menunjukkan cara mengambil variabel luar yang direferensikan dalam isi lambda: [&] berarti semua variabel yang Anda rujuk ditangkap berdasarkan referensi, dan [=] berarti variabel tersebut diambil berdasarkan nilai. Anda dapat menggunakan mode pengambilan default, lalu menentukan mode yang berlawanan secara eksplisit untuk variabel tertentu. Misalnya, jika isi lambda mengakses variabel total eksternal berdasarkan referensi dan variabel factor eksternal menurut nilai, maka klausa pengambilan berikut setara:

[&total, factor]
[factor, &total]
[&, factor]
[=, &total]

Hanya variabel yang disebutkan dalam isi lambda yang ditangkap saat capture-default digunakan.

Jika klausul pengambilan menyertakan capture-default &, maka tidak ada pengidentifikasi dalam pengambilan klausa pengambilan tersebut yang dapat memiliki formulir &identifier. Demikian juga, jika klausul pengambilan menyertakan capture-default =, maka tidak ada pengambilan klausa pengambilan tersebut yang dapat memiliki formulir =identifier. Pengidentifikasi atau this tidak dapat muncul lebih dari sekali dalam klausa pengambilan. Cuplikan kode berikut mengilustrasikan beberapa contoh:

struct S { void f(int i); };

void S::f(int i) {
    [&, i]{};      // OK
    [&, &i]{};     // ERROR: i preceded by & when & is the default
    [=, this]{};   // ERROR: this when = is the default
    [=, *this]{ }; // OK: captures this by value. See below.
    [i, i]{};      // ERROR: i repeated
}

Tangkapan diikuti oleh elipsis adalah ekspansi paket, seperti yang ditunjukkan dalam contoh templat variadik ini:

template<class... Args>
void f(Args... args) {
    auto x = [args...] { return g(args...); };
    x();
}

Untuk menggunakan ekspresi lambda dalam isi fungsi anggota kelas, teruskan this penunjuk ke klausul pengambilan untuk memberikan akses ke fungsi anggota dan anggota data kelas penutup.

Visual Studio 2017 versi 15.3 dan yang lebih baru (tersedia dalam /std:c++17 mode dan yang lebih baru): Penunjuk this dapat diambil berdasarkan nilai dengan menentukan *this dalam klausa pengambilan. Tangkap berdasarkan nilai menyalin seluruh penutupan ke setiap situs panggilan tempat lambda dipanggil. (Penutupan adalah objek fungsi anonim yang merangkum ekspresi lambda.) Tangkap berdasarkan nilai berguna ketika lambda dijalankan dalam operasi paralel atau asinkron. Ini sangat berguna pada arsitektur perangkat keras tertentu, seperti NUMA.

Untuk contoh yang menunjukkan cara menggunakan ekspresi lambda dengan fungsi anggota kelas, lihat "Contoh: Menggunakan ekspresi lambda dalam metode" dalam Contoh ekspresi lambda.

Saat Anda menggunakan klausa pengambilan, kami sarankan Anda mengingat poin-poin ini, terutama ketika Anda menggunakan lambda dengan multi-utas:

  • Pengambilan referensi dapat digunakan untuk memodifikasi variabel di luar, tetapi pengambilan nilai tidak dapat. (mutable memungkinkan salinan untuk dimodifikasi, tetapi tidak asli.)

  • Pengambilan referensi mencerminkan pembaruan pada variabel di luar, tetapi pengambilan nilai tidak.

  • Pengambilan referensi memperkenalkan dependensi seumur hidup, tetapi tangkapan nilai tidak memiliki dependensi seumur hidup. Sangat penting ketika lambda berjalan secara asinkron. Jika Anda menangkap lokal dengan referensi dalam asinkron lambda, lokal tersebut dapat dengan mudah hilang pada saat lambda berjalan. Kode Anda dapat menyebabkan pelanggaran akses pada waktu proses.

Pengambilan umum (C++14)

Di C++14, Anda dapat memperkenalkan dan menginisialisasi variabel baru dalam klausul pengambilan, tanpa perlu memiliki variabel tersebut dalam cakupan penutup fungsi lambda. Inisialisasi dapat dinyatakan sebagai ekspresi sewenang-wenang; jenis variabel baru disimpulkan dari jenis yang dihasilkan oleh ekspresi. Fitur ini memungkinkan Anda mengambil variabel move-only (seperti std::unique_ptr) dari cakupan sekitarnya dan menggunakannya dalam lambda.

pNums = make_unique<vector<int>>(nums);
//...
      auto a = [ptr = move(pNums)]()
        {
           // use ptr
        };

Daftar parameter

Lambda dapat mengambil variabel dan menerima parameter input. Daftar parameter (deklarator lambda dalam sintaks Standar) bersifat opsional dan dalam sebagian besar aspek menyerupai daftar parameter untuk fungsi.

auto y = [] (int first, int second)
{
    return first + second;
};

Di C++14, jika jenis parameter umum, Anda dapat menggunakan auto kata kunci sebagai penentu jenis. Kata kunci ini memberi tahu pengkompilasi untuk membuat operator panggilan fungsi sebagai templat. Setiap instans auto dalam daftar parameter setara dengan parameter jenis yang berbeda.

auto y = [] (auto first, auto second)
{
    return first + second;
};

Ekspresi lambda dapat mengambil ekspresi lambda lain sebagai argumennya. Untuk informasi selengkapnya, lihat "Ekspresi Lambda Dengan Urutan Lebih Tinggi" dalam artikel Contoh ekspresi lambda.

Karena daftar parameter bersifat opsional, Anda dapat menghilangkan tanda kurung kosong jika Anda tidak meneruskan argumen ke ekspresi lambda dan deklarator lambda-nya tidak berisi spesifikasi pengecualian, trailing-return-type, atau mutable.

Spesifikasi yang dapat diubah

Biasanya, operator panggilan fungsi lambda adalah const-by-value, tetapi penggunaan mutable kata kunci membatalkan ini. Ini tidak menghasilkan anggota data yang dapat diubah. Spesifikasi memungkinkan mutable isi ekspresi lambda untuk memodifikasi variabel yang ditangkap oleh nilai. Beberapa contoh nanti dalam artikel ini menunjukkan cara menggunakan mutable.

Spesifikasi pengecualian

Anda dapat menggunakan noexcept spesifikasi pengecualian untuk menunjukkan bahwa ekspresi lambda tidak melemparkan pengecualian apa pun. Seperti fungsi biasa, kompilator Microsoft C++ menghasilkan peringatan C4297 jika ekspresi lambda mendeklarasikan noexcept spesifikasi pengecualian dan isi lambda memunculkan pengecualian, seperti yang ditunjukkan di sini:

// throw_lambda_expression.cpp
// compile with: /W4 /EHsc
int main() // C4297 expected
{
   []() noexcept { throw 5; }();
}

Untuk informasi selengkapnya, lihat Spesifikasi pengecualian (throw).

Jenis Pengembalian

Jenis pengembalian ekspresi lambda secara otomatis disimpulkan. Anda tidak perlu menggunakan auto kata kunci kecuali anda menentukan trailing-return-type. Trailing-return-type menyerupan bagian jenis pengembalian dari fungsi biasa atau fungsi anggota. Namun, jenis pengembalian harus mengikuti daftar parameter, dan Anda harus menyertakan kata kunci -> trailing-return-type sebelum jenis pengembalian.

Anda dapat menghilangkan bagian jenis kembali ekspresi lambda jika isi lambda hanya berisi satu pernyataan pengembalian. Atau, jika ekspresi tidak mengembalikan nilai. Jika isi lambda berisi satu pernyataan pengembalian, pengkompilasi menyimpulkan jenis pengembalian dari jenis ekspresi pengembalian. Jika tidak, pengkompilasi menyimpulkan jenis pengembalian sebagai void. Pertimbangkan contoh cuplikan kode berikut yang menggambarkan prinsip ini:

auto x1 = [](int i){ return i; }; // OK: return type is int
auto x2 = []{ return{ 1, 2 }; };  // ERROR: return type is void, deducing
                                  // return type from braced-init-list isn't valid

Ekspresi lambda dapat menghasilkan ekspresi lambda lain sebagai nilai pengembaliannya. Untuk informasi selengkapnya, lihat "Ekspresi lambda dengan urutan lebih tinggi" dalam Contoh ekspresi lambda.

Tubuh Lambda

Isi lambda ekspresi lambda adalah pernyataan majemuk. Ini dapat berisi apa pun yang diizinkan dalam isi fungsi biasa atau fungsi anggota. Isi fungsi biasa dan ekspresi lambda dapat mengakses jenis variabel ini:

  • Variabel yang diambil dari cakupan penutup, seperti yang dijelaskan sebelumnya.

  • Parameter.

  • Variabel yang dideklarasikan secara lokal.

  • Anggota data kelas, saat dideklarasikan di dalam kelas dan this ditangkap.

  • Variabel apa pun yang memiliki durasi penyimpanan statis—misalnya, variabel global.

Contoh berikut berisi ekspresi lambda yang secara eksplisit menangkap variabel n berdasarkan nilai dan secara implisit menangkap variabel m berdasarkan referensi:

// captures_lambda_expression.cpp
// compile with: /W4 /EHsc
#include <iostream>
using namespace std;

int main()
{
   int m = 0;
   int n = 0;
   [&, n] (int a) mutable { m = ++n + a; }(4);
   cout << m << endl << n << endl;
}
5
0

Karena variabel n diambil berdasarkan nilai, nilainya tetap 0 setelah panggilan ke ekspresi lambda. Spesifikasinya mutable memungkinkan n untuk dimodifikasi dalam lambda.

Ekspresi lambda hanya dapat mengambil variabel yang memiliki durasi penyimpanan otomatis. Namun, Anda dapat menggunakan variabel yang memiliki durasi penyimpanan statis dalam isi ekspresi lambda. Contoh berikut menggunakan generate fungsi dan ekspresi lambda untuk menetapkan nilai ke setiap elemen dalam vector objek. Ekspresi lambda memodifikasi variabel statis untuk menghasilkan nilai elemen berikutnya.

void fillVector(vector<int>& v)
{
    // A local static variable.
    static int nextValue = 1;

    // The lambda expression that appears in the following call to
    // the generate function modifies and uses the local static
    // variable nextValue.
    generate(v.begin(), v.end(), [] { return nextValue++; });
    //WARNING: this isn't thread-safe and is shown for illustration only
}

Untuk informasi selengkapnya, lihat membuat.

Contoh kode berikut menggunakan fungsi dari contoh sebelumnya, dan menambahkan contoh ekspresi lambda yang menggunakan algoritma generate_nPustaka Standar C++ . Ekspresi lambda ini menetapkan elemen vector objek ke jumlah dari dua elemen sebelumnya. Kata mutable kunci digunakan sehingga isi ekspresi lambda dapat memodifikasi salinan variabel x eksternal dan y, yang ditangkap oleh ekspresi lambda berdasarkan nilai. Karena ekspresi lambda menangkap variabel x asli dan y berdasarkan nilai, nilainya tetap 1 ada setelah lambda dijalankan.

// compile with: /W4 /EHsc
#include <algorithm>
#include <iostream>
#include <vector>
#include <string>

using namespace std;

template <typename C> void print(const string& s, const C& c) {
    cout << s;

    for (const auto& e : c) {
        cout << e << " ";
    }

    cout << endl;
}

void fillVector(vector<int>& v)
{
    // A local static variable.
    static int nextValue = 1;

    // The lambda expression that appears in the following call to
    // the generate function modifies and uses the local static
    // variable nextValue.
    generate(v.begin(), v.end(), [] { return nextValue++; });
    //WARNING: this isn't thread-safe and is shown for illustration only
}

int main()
{
    // The number of elements in the vector.
    const int elementCount = 9;

    // Create a vector object with each element set to 1.
    vector<int> v(elementCount, 1);

    // These variables hold the previous two elements of the vector.
    int x = 1;
    int y = 1;

    // Sets each element in the vector to the sum of the
    // previous two elements.
    generate_n(v.begin() + 2,
        elementCount - 2,
        [=]() mutable throw() -> int { // lambda is the 3rd parameter
        // Generate current value.
        int n = x + y;
        // Update previous two values.
        x = y;
        y = n;
        return n;
    });
    print("vector v after call to generate_n() with lambda: ", v);

    // Print the local variables x and y.
    // The values of x and y hold their initial values because
    // they are captured by value.
    cout << "x: " << x << " y: " << y << endl;

    // Fill the vector with a sequence of numbers
    fillVector(v);
    print("vector v after 1st call to fillVector(): ", v);
    // Fill the vector with the next sequence of numbers
    fillVector(v);
    print("vector v after 2nd call to fillVector(): ", v);
}
vector v after call to generate_n() with lambda: 1 1 2 3 5 8 13 21 34
x: 1 y: 1
vector v after 1st call to fillVector(): 1 2 3 4 5 6 7 8 9
vector v after 2nd call to fillVector(): 10 11 12 13 14 15 16 17 18

Untuk informasi selengkapnya, lihat generate_n.

constexpr ekspresi lambda

Visual Studio 2017 versi 15.3 dan yang lebih baru (tersedia dalam /std:c++17 mode dan yang lebih baru): Anda dapat mendeklarasikan ekspresi lambda sebagai constexpr (atau menggunakannya dalam ekspresi konstan) ketika inisialisasi setiap anggota data yang diambil atau diperkenalkan diizinkan dalam ekspresi konstan.

    int y = 32;
    auto answer = [y]() constexpr
    {
        int x = 10;
        return y + x;
    };

    constexpr int Increment(int n)
    {
        return [n] { return n + 1; }();
    }

Lambda secara constexpr implisit jika hasilnya memenuhi persyaratan constexpr fungsi:

    auto answer = [](int n)
    {
        return 32 + n;
    };

    constexpr int response = answer(10);

Jika lambda secara implisit atau eksplisit constexpr, konversi ke penunjuk fungsi menghasilkan constexpr fungsi:

    auto Increment = [](int n)
    {
        return n + 1;
    };

    constexpr int(*inc)(int) = Increment;

Khusus Microsoft

Lambdas tidak didukung dalam entitas terkelola common language runtime (CLR) berikut: ref class, , ref struct, value classatau value struct.

Jika Anda menggunakan pengubah khusus Microsoft seperti __declspec, Anda dapat menyisipkannya ke dalam ekspresi lambda segera setelah parameter-declaration-clause. Misalnya:

auto Sqr = [](int t) __declspec(code_seg("PagedMem")) -> int { return t*t; };

Untuk menentukan apakah pengubah tertentu didukung oleh lambda, lihat artikel tentang pengubah di bagian Pengubah khusus Microsoft.

Visual Studio mendukung fungsionalitas lambda Standar C++11, dan lambda tanpa status. Lambda stateless dapat dikonversi ke penunjuk fungsi yang menggunakan konvensi panggilan arbitrer.

Baca juga

Referensi Bahasa C++
Objek Fungsi di Pustaka Standar C++
Panggilan Fungsi
for_each