Section Fungsi sudah memperkenalkan tiga pilar lambda: anatomi [capture](parameter) { badan } pada chapter Lambda Expressions, perilaku capture by value dan by reference pada chapter Capture Lambda, dan parameter auto pada chapter Generic Lambda. Pilar itu cukup untuk kode sehari-hari, tetapi fitur-fiturnya berhenti di tahun 2011; standar-standar berikutnya menyempurnakan lambda menjadi alat yang bisa menghitung saat didefinisikan, memindahkan data masuk tanpa salinan, dan menuliskan parameter template secara eksplisit. Chapter ini menutup celah tersebut: init-capture dan move-capture (C++14), template parameter lambda (C++20), perilaku mutable yang kita uji ulang, dan pola immediately-invoked lambda untuk menghitung konstanta sekali jalan. Semua fitur diuji langsung dengan g++ 14.2 — inilah salah satu untungnya belajar C++ modern: fitur yang kita bahas bisa langsung dibuktikan, bukan sekadar dibaca.
Recap: Lambda Sebenarnya Apa #
Sebelum menambah fitur, kita sepakat dulu mekanismenya. Ketika compiler melihat sebuah lambda, compiler membangun class kecil tanpa nama (closure type) yang memiliki operator(), lalu membuat objeknya — closure object. Setiap variabel yang di-capture menjadi member dari objek itu: [total] menyalin total ke member saat lambda didefinisikan, [&total] menyimpan reference ke total, dan parameter auto membuat operator() menjadi template member.
Dari mekanisme ini semua fitur bab ini bisa dijelaskan tanpa kejutan. Karena member diisi saat lambda didefinisikan, menulis [x = ekspresi] berarti “isi member bernama x dengan hasil ekspresi saat ini” — fleksibilitas yang tidak dimiliki [x] yang hanya menyalin variabel yang sudah ada. Karena operator() bisa dibuat template, compiler bisa menerima sintaks template eksplisit di dalam capture list. Dan karena capture by value menyalin, mengubah member dari dalam operator() memicu aturan const yang hanya bisa dilewati dengan mutable. Dengan peta mental ini, mari kita lihat programnya.
Program Fitur Modern #
// lambda-modern.cpp — fitur lambda modern: init-capture, template parameter, mutable, IIFE
// Kompilasi: g++ -std=c++23 -Wall -Wextra lambda-modern.cpp -o lambda-modern
#include <print>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
int hitungTotal(const std::vector<int>& v) {
int total = 0;
for (int x : v) total += x;
return total;
}
int main() {
std::vector<int> angka{10, 20, 30, 40};
// 1) init-capture (C++14): hitung SEKALI saat lambda didefinisikan
std::print("-- init-capture\n");
auto laporan = [total = hitungTotal(angka)]() {
return "total = " + std::to_string(total);
};
std::print("{}\n", laporan());
// 2) move-capture: string berat pindah masuk tanpa salinan
std::print("-- move-capture\n");
std::string berat(10, 'x'); // "xxxxxxxxxx"
auto bawa = [isi = std::move(berat)]() {
return "isi: " + isi;
};
std::print("{}\n", bawa());
// 3) template parameter lambda (C++20): sintaks <typename T> eksplisit
std::print("-- template parameter lambda\n");
auto proses = []<typename T>(T nilai) {
if constexpr (std::is_integral_v<T>) {
return nilai * 100; // jalur bilangan bulat
} else {
return std::string(nilai); // jalur lain: bungkus jadi string
}
};
std::print("int : {}\n", proses(7));
std::print("cstr: {}\n", proses("halo"));
// 4) mutable: boleh mengubah SALINAN yang di-capture by value
std::print("-- mutable\n");
int panggilan = 0;
auto catat = [panggilan]() mutable { return ++panggilan; };
catat();
catat();
std::print("internal = {}\n", catat());
std::print("eksternal = {}\n", panggilan);
// 5) immediately-invoked lambda: hitung konstanta sekali jalan
std::print("-- immediately-invoked lambda\n");
const int batas = [] {
int total = 0;
for (int i = 1; i <= 4; ++i) total += i * i;
return total;
}();
std::print("batas = {}\n", batas);
return 0;
}Output yang Dihasilkan:
-- init-capture
total = 100
-- move-capture
isi: xxxxxxxxxx
-- template parameter lambda
int : 700
cstr: halo
-- mutable
internal = 3
eksternal = 0
-- immediately-invoked lambda
batas = 30Mari kita bedah:
- Baris 21-24 (init-capture):
[total = hitungTotal(angka)]mendeklarasikan member baru bernamatotalyang diisi sekali — saat lambda didefinisikan — dengan hasil pemanggilanhitungTotal(angka). Nama member tidak harus sama dengan variabel mana pun; ini adalah deklarasi sungguhan di dalam capture list. - Baris 28-32 (move-capture):
[isi = std::move(berat)]memindahkan buffer"xxxxxxxxxx"masuk ke memberisitanpa menyalinnya. Pola ini khas untuk objek berat sepertistd::stringpanjang ataustd::vectorbesar yang tak lagi dibutuhkan di luar lambda. - Baris 36-44 (template parameter lambda):
[]<typename T>(T nilai)memberi nama eksplisitTpada tipe parameter. Di dalam badan,if constexprmemilih jalur saat kompilasi:proses(7)masuk cabang integral dan mencetak700, sedangkanproses("halo")masuk cabangstd::stringdan mencetakhalo. - Baris 49-53 (mutable):
[panggilan]menyalin0ke member; tanpamutable, member itu const dan++panggilanditolak compiler. Denganmutable, setiap pemanggilan menaikkan salinan internal hingga tercetak3, sedangkan variabel asli tetap0— bukti bahwa yang berubah adalah member, bukan sumbernya. - Baris 57-62 (immediately-invoked lambda): lambda didefinisikan dan langsung dipanggil dengan
()di akhir, sehinggabatasdiisi hasil1 + 4 + 9 + 16 = 30sekali jalan. Tanpa lambda, kita perlu fungsi helper bernama atau variabel sementara yang bocor ke scopemain().
Init-capture: Menghitung Saat Didefinisikan #
Perbedaan [total = hitungTotal(angka)] dengan pola lama seperti [total] terletak pada waktu dan sumbernya. Capture klasik [total] hanya bisa menyalin variabel yang sudah ada dengan nama yang sama; init-capture mendeklarasikan member baru dari ekspresi apa pun — hasil fungsi, std::move, bahkan angka literal seperti [pajak = 0.11]. Ekspresinya dievaluasi tepat satu kali, saat lambda diciptakan, bukan setiap kali lambda dipanggil.
Perbedaan waktu ini punya konsekuensi nyata. Lambda klasik yang capture by reference ke variabel lokal berbahaya bila lambda hidup lebih lama dari fungsi pembuatnya (dangling reference — sudah kita bahas di chapter Capture Lambda); dengan init-capture, kita bisa memindahkan salinan masuk dan lambda jadi mandiri. Perhatikan juga penamaannya: member bernama isi tidak terikat lagi dengan berat di luar, sehingga membaca lambda tidak menuntut pembaca mengikuti nama-nama di sekitarnya. Untuk kode yang dipindahkan (std::move), ini bukan sekadar gaya — ini cara memberi lambda kepemilikan data.
Template Parameter Lambda #
Parameter auto pada lambda generik (C++14) sudah nyaman, tetapi kita tidak bisa menyebut tipe itu secara eksplisit. Sintaks C++20 []<typename T>(T nilai) memperbaikinya: T menjadi nama yang bisa dipakai di dalam badan — untuk if constexpr, static_assert, alias tipe, atau menduksi tipe berkaitan seperti typename T::value_type. Sintaksnya sengaja dibuat mirip function template biasa, termasuk versi multi-parameter []<typename T, typename U>(T a, U b).
Kapan auto tidak cukup? Contoh pada baris 36 sebenarnya masih bisa ditulis dengan auto, tetapi begitu logika membutuhkan identitas tipe — misalnya memastikan dua parameter bertipe sama, atau menghitung std::vector<T> dari elemen T — template parameter adalah satu-satunya jalan yang jelas. Di kode generic nyata (algoritma custom, wrapper view), sintaks ini muncul hampir selalu; di kode harian, auto tetap pilihan pertama. Mengenali keduanya membuat kita bisa membaca kode pustaka modern tanpa tersangkut.
Immediately-Invoked Lambda (IIFE) #
Pola [] { ... }() — lambda yang langsung dipanggil — mengubah lambda dari “fungsi yang disimpan” menjadi “blok yang mengembalikan nilai”. Kegunaan utamanya adalah menginisialisasi const dengan logika bersyarat: karena const wajib diisi saat deklarasi, kita tidak boleh mengisinya di beberapa langkah berikut, dan di sinilah IIFE menyelesaikan masalah dengan rapi — seluruh logika hidup di satu ekspresi, hasilnya langsung jadi nilai awal.
Pola ini juga menjaga scope tetap bersih. Bandingkan dengan deklarasi variabel bantu int total = 0; for (...) ... yang hidup sampai akhir fungsi: versi IIFE membatasi semua variabel bantu di dalam lambda, sehingga main() hanya berisi batas yang benar-benar dibutuhkan. Compiler pun mengoptimalkannya tanpa sisa — hasilnya sama dengan menulis logika inline, tetapi strukturnya jauh lebih terbaca. Anggap IIFE sebagai “blok berlabel kembalian” yang sering menggantikan fungsi helper yang hanya dipakai satu kali.
Satu langkah dari baris 29 layak digarisbawahi: std::move(berat) memindahkan isi string masuk ke lambda, dan di situlah kita menyentuh mekanisme move semantics yang selama ini hanya lewat. Chapter berikutnya membahasnya tuntas — apa yang sebenarnya terjadi saat data “dipindahkan”, kapan compiler melakukannya otomatis, dan bagaimana std::move bekerja.
Rujukan resmi: cppreference — lambda expressions .