Lambda pada chapter sebelumnya bekerja dengan parameter yang dikirim pemanggil — sama seperti fungsi biasa. Kekuatan sebenarnya baru muncul ketika lambda diizinkan mengakses variabel yang hidup di lingkungannya: koefisien, penghitung, atau status yang dipakai ulang. Mekanisme itu dinamai capture, ditulis di dalam kurung siku [ ] di awal lambda. Pada Chapter ini kita membedah capture by value dan capture by reference, melihat konsekuensi masing-masing lewat pengujian nyata, dan menetapkan kapan setiap bentuk aman dipakai.

Capture by Value: Salinan Saat Didefinisikan #

[dasar, penyesuai] menyalin nilai kedua variabel ke dalam lambda pada saat lambda dibuat. Setelah itu lambda hidup dengan salinannya sendiri — perubahan pada variabel asli tidak memengaruhi isi lambda.

cpp
#include <iostream>

int main() {
    int dasar = 100;
    int penyesuai = 5;

    // capture by value: salinan dibuat saat lambda didefinisikan
    auto total = [dasar, penyesuai]() {
        return dasar + penyesuai;
    };
    dasar = 999;  // perubahan ini TIDAK memengaruhi salinan di dalam lambda
    std::cout << "by value    : " << total() << "\n";

    // capture by reference: lambda melihat nilai terkini
    int sisi = 4;
    auto luas = [&sisi]() {
        return sisi * sisi;
    };
    sisi = 10;  // perubahan ini terlihat oleh lambda
    std::cout << "by reference: " << luas() << "\n";

    // [=] menangkap semua by value, [&] semua by reference
    int a = 1, b = 2;
    auto jumlah = [=]() { return a + b; };
    auto kali   = [&]() { return a * b; };
    std::cout << "[=] a+b: " << jumlah() << ", [&] a*b: " << kali() << "\n";

    return 0;
}

Output yang Dihasilkan:

text
by value    : 105
by reference: 100
[=] a+b: 3, [&] a*b: 2

Mari kita telaah apa yang terjadi pada baris-baris kode di atas:

  1. Baris 9-11 ([dasar, penyesuai]): compiler menyisipkan dua data member (salinan 100 dan 5) ke dalam objek lambda. Baris 12 mengubah dasar menjadi 999, tetapi panggilan total() tetap menghasilkan 105 karena lambda memakai salinannya.
  2. Baris 16-18 ([&sisi]): lambda menyimpan alamat sisi, bukan salinannya. Baris 19 mengubah sisi menjadi 10, sehingga luas() menghitung 10 × 10 = 100 — lambda selalu membaca nilai terkini.
  3. Baris 23-24 ([=] dan [&]): bentuk singkat untuk menangkap semua variabel yang dipakai lambda. [=] menyalin semuanya (a + b = 3), [&] meminjam semuanya (a * b = 2).

Perbedaan hasil 105 lawan 100 adalah inti chapter ini: pilihan capture menentukan kapan lambda “memotret” nilai atau “menyaksikan” perubahannya.

Kata Kunci mutable #

Salinan di dalam lambda by value bersifat konstan — compiler melarang badan lambda mengubahnya. Bila memang perlu, tambahkan kata kunci mutable setelah daftar parameter; lambda lalu memiliki salinan yang boleh berubah, dan perubahannya bertahan antar pemanggilan karena tersimpan di objek lambda itu sendiri.

cpp
#include <iostream>

int main() {
    int panggilan = 0;

    // mutable: salinan 'panggilan' boleh diubah di dalam lambda
    auto hitung = [panggilan]() mutable {
        ++panggilan;
        return panggilan;
    };

    std::cout << hitung() << "\n";  // 1
    std::cout << hitung() << "\n";  // 2
    std::cout << hitung() << "\n";  // 3

    // variabel asli tidak tersentuh
    std::cout << "asli: " << panggilan << "\n";  // 0

    return 0;
}

Output yang Dihasilkan:

text
1
2
3
asli: 0

Mari kita telaah: Baris 7 ([panggilan]() mutable) membuat lambda menyimpan salinan panggilan yang dapat diubah. Baris 9-12 memanggilnya tiga kali; setiap panggilan menaikkan salinan di dalam objek lambda sehingga hasilnya 1, 2, 3. Baris 15 membuktikan variabel asli tetap 0 — mutable mengubah salinan, bukan sumbernya.

Bahaya Capture by Reference #

Capture by reference tidak menyalin apa pun, sehingga biayanya nyaris nol — tetapi itu juga sumber bug yang paling sering ditemui: bila lambda bertahan lebih lama dari variabel yang dipinjamnya, lambda memegang reference ke memori yang sudah tidak sah. Pola ini muncul saat lambda disimpan untuk dipanggil belakangan (callback, thread, event handler), bukan saat dipanggil langsung seperti contoh di atas.

cpp Pola yang harus dihindari (lambda lebih lama dari variabelnya)
auto buatLambda() {
    int lokal = 42;
    return [&lokal]() { return lokal; };  // meminjam 'lokal'
}
// setelah fungsi selesai, 'lokal' tidak hidup lagi
// memanggil lambda yang dikembalikan = undefined behavior

Snippet di atas disajikan sebagai pola yang harus dihindari, bukan program yang dijalankan: begitu buatLambda() selesai, variabel lokal berakhir bersama frame fungsinya, dan lambda yang dikembalikan menunjuk ke memori yang sudah tidak sah. Pemanggilannya adalah undefined behavior — perilakunya tidak terdefinisi dan tidak boleh diandalkan dalam bentuk apa pun.

Dengan capture, lambda berhenti menjadi sekadar fungsi singkat dan berubah menjadi objek kecil yang membawa data. Chapter berikutnya melangkah lebih jauh: lambda generic yang parameternya menyesuaikan tipe — jembatan alami menuju template pada chapter penutup Section ini.