Lambda dan std::function

Capture by value dan reference, mutable, serta penyimpanan callable.

Daftar isi

Topik ini membahas lambda dan std::function sebagai dua perkakas untuk bekerja dengan fungsi sebagai nilai. Pembahasan mencakup anatomi lambda, capture by value dan by reference, kata kunci mutable, serta penyimpanan callable yang tipenya berbeda-beda dalam satu wadah. Setelah membacanya, kriteria algoritma dapat ditulis langsung pada tempat pemanggilan dan callback dapat disimpan tanpa mendefinisikan kelas fungsi baru.

Anatomi lambda dan closure#

Lambda adalah fungsi tanpa nama yang didefinisikan langsung pada tempat pemanggilannya. Bentuknya diawali tanda kurung siku berisi daftar capture, diikuti parameter, lalu isi fungsinya. Compiler menerjemahkan setiap lambda menjadi sebuah kelas dengan operator(), dan objek yang dibuat dari kelas itu disebut closure, yaitu objek fungsi yang membawa isinya sendiri. Karena berupa objek, lambda dapat disimpan pada variabel, diteruskan sebagai argumen, dan dikembalikan dari fungsi.

cpp anatomi-lambda.cpp
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> nilai = {85, 60, 90, 70};

    // lambda disimpan pada variabel lalu dipanggil
    auto sapa = []() {
        std::cout << "Rekap nilai\n";
    };
    sapa();

    // lambda sebagai kriteria sort: urutan menurun
    std::sort(nilai.begin(), nilai.end(),
              [](int a, int b) { return a > b; });

    for (int n : nilai) {
        std::cout << n << '\n';
    }
    return 0;
}
text Keluaran
Rekap nilai
90
85
70
60

Lambda pertama disimpan pada variabel sapa lalu dipanggil seperti fungsi biasa. Lambda kedua diteruskan langsung sebagai argumen ketiga std::sort dan menentukan bahwa elemen a harus mendahului b bila a > b bernilai true, sehingga urutannya menurun. Tipe variabelnya ditulis auto karena tipe closure yang dibuat compiler bersifat unik dan tidak dapat dituliskan secara manual.

Parameter lambda ditulis sebagaimana fungsi biasa, dan tipe kembalinya biasanya disimpulkan dari isi badannya. Bila perlu ditegaskan, tipe kembalian ditulis setelah parameter dengan bentuk -> tipe, misalnya [](int n) -> double { return n / 2.0; }. Lambda yang tidak memcapture apa pun juga dapat dikonversi menjadi pointer fungsi biasa, sehingga ia dapat dipakai pada antarmuka lama yang menuntut pointer fungsi.

Capture by value dan by reference#

Daftar capture mengatur variabel luar mana yang boleh dipakai di dalam lambda. Tanpa capture, lambda hanya melihat parameternya sendiri. Menuliskan nama variabel di dalam tanda kurung siku membuat salinannya dibawa masuk, yaitu capture by value, sedangkan tanda & di depan nama membuat lambda melihat variabel aslinya, yaitu capture by reference. Bentuk ringkasnya [=] untuk semua by value dan [&] untuk semua by reference, tetapi daftar eksplisit lebih disarankan karena memperlihatkan secara langsung variabel mana yang dibawa lambda. Di dalam member function, lambda dapat memcapture this untuk mengakses anggota kelas, dan keberadaan objek kelasnya tetap wajib dijaga sebagaimana capture by reference.

cpp capture-nilai-reference.cpp
#include <iostream>

int main() {
    int dipanggil = 0;

    // capture by value: salinan dibuat saat lambda didefinisikan
    auto denganNilai = [dipanggil]() {
        return dipanggil;
    };

    // capture by reference: variabel asli yang dilihat
    auto denganReferensi = [&dipanggil]() {
        return dipanggil;
    };

    dipanggil = 5;
    std::cout << "by value    : " << denganNilai() << '\n';
    std::cout << "by reference: " << denganReferensi() << '\n';
    return 0;
}
text Keluaran
by value    : 0
by reference: 5

Salinan by value dibuat pada saat lambda didefinisikan, sehingga perubahan dipanggil sesudahnya tidak terlihat dari dalam lambda. Capture by reference tidak menyalin apa pun dan selalu membaca keadaan terbaru variabel aslinya. Konsekuensinya, capture by reference hanya aman selama variabel yang dirujuk masih hidup. Pola keliru berikut memperlihatkan apa yang terjadi bila syarat itu dilanggar.

cpp Pola keliru: lambda yang dikembalikan memakai capture by reference
// tidak-dijalankan (undefined behavior: referensi menggantung)
auto buatNomor() {
    int hitungan = 0;
    return [&hitungan]() { return ++hitungan; };   // hitungan mati saat fungsi kembali
}

mutable untuk keadaan milik lambda#

Operator pemanggilan pada closure dibuat konstan oleh compiler, sehingga salinan by value di dalamnya tidak boleh diubah. Kata kunci mutable melonggarkan larangan tersebut dan membuat closure menyimpan keadaan yang boleh berubah antarpemanggilan. Pola ini berguna untuk penghitung pemanggilan, penomoran berurutan, dan cache kecil tanpa perlu kelas terpisah.

cpp mutable-lambda.cpp
#include <iostream>

int main() {
    int hitungan = 0;

    // mutable: salinan hitungan milik lambda boleh berubah
    auto nomorBerikutnya = [hitungan]() mutable {
        return ++hitungan;
    };

    std::cout << nomorBerikutnya() << '\n';
    std::cout << nomorBerikutnya() << '\n';
    std::cout << nomorBerikutnya() << '\n';
    std::cout << "Hitungan asli: " << hitungan << '\n';
    return 0;
}
text Keluaran
1
2
3
Hitungan asli: 0

Setiap pemanggilan nomorBerikutnya() menaikkan salinan hitungan milik closure, bukan variabel aslinya, sehingga keluarannya berurutan 1, 2, 3 sedangkan hitungan di luar tetap 0. Keadaan itu milik masing-masing closure, sehingga menyalin closure berarti menyalin keadaannya pula dan dua salinan berjalan dengan riwayat yang terpisah.

Penyimpanan callable dengan std::function#

Setiap lambda memiliki tipe closure yang unik, sehingga dua lambda dengan isi berbeda tidak dapat dimasukkan ke satu vector tanpa tipe perantara. std::function dari header <functional> menutup masalah itu lewat penghapusan tipe (type erasure), yaitu teknik membungkus callable apa pun yang tanda tangannya cocok menjadi satu tipe tunggal. Objek std::function<int(int)> misalnya dapat menyimpan lambda, pointer fungsi, maupun objek fungsi buatan sendiri, selama semuanya menerima int dan mengembalikan int.

cpp std-function-penyimpan.cpp
#include <functional>
#include <iostream>
#include <vector>

int main() {
    // callable yang berbeda disimpan dalam satu tipe yang sama
    std::vector<std::function<int(int)>> operasi;

    operasi.push_back([](int n) { return n + 10; });
    operasi.push_back([](int n) { return n * 2; });

    int data = 5;
    for (const auto& op : operasi) {
        data = op(data);
        std::cout << data << '\n';
    }
    return 0;
}
text Keluaran
15
30

Dua lambda berbeda ditambahkan ke vector yang sama dan dipanggil bergantian dengan tanda tangan yang seragam. Fleksibilitas ini ada biayanya, karena std::function berpotensi mengalokasi memori dinamis dan memanggil callable yang dibungkusnya secara tidak langsung. Bila tipe callable sudah pasti pada waktu kompilasi, parameter template atau variabel auto lebih ringan, sedangkan std::function dipilih ketika tipenya perlu ditulis pada antarmuka atau disimpan campur dalam satu wadah.

Kesalahan umum#

Empat kesalahan berikut paling sering muncul pada pemakaian lambda.

  1. Memakai [&] sebagai default capture pada lambda yang disimpan lama atau dikirim keluar fungsi. Referensinya mudah menggantung setelah variabel asalnya mati. Capture eksplisit by value lebih aman untuk lambda yang menyeberangi lingkup.
  2. Mengubah capture by value tanpa mutable. Compiler menolaknya karena operator() pada closure bersifat konstan. Tambahkan mutable atau ubah capture menjadi by reference.
  3. Menganggap dua lambda bertipe sama karena bentuknya mirip. Tipe closure-nya berbeda, sehingga satu vector tidak dapat menampung keduanya tanpa tipe perantara. Simpan keduanya dalam std::function yang tanda tangannya cocok.
  4. Memanggil std::function yang belum diisi. Objek kosong yang dipanggil melempar exception std::bad_function_call. Uji dengan member function operator bool() sebelum dipanggil bila keadaannya tidak pasti.

Ringkasan#

  • Lambda adalah closure yang dibuat compiler, sehingga ia dapat disimpan pada variabel, diteruskan ke algoritma, dan membawa variabel luar lewat daftar capture.
  • Capture by value menyalin keadaan saat lambda didefinisikan dan butuh mutable bila keadaannya akan berubah, sedangkan capture by reference selalu membaca variabel aslinya dan berisiko menggantung.
  • std::function menyimpan callable apa pun dengan tanda tangan yang cocok, dengan biaya pemanggilan yang tidak langsung untuk dipertimbangkan.

Referensi#