Pada Chapter Function Pointer kita menyimpan alamat fungsi ke dalam variabel dan mengirimnya sebagai callback — tetapi tipenya kaku: int (*)(int) hanya menerima fungsi dengan tanda tangan persis itu, dan lambda tidak bisa masuk karena tipenya anonim. Dalam program nyata kita sering butuh kebalikannya: satu variabel anggota, satu parameter, atau satu slot std::vector yang bisa menampung callable apa pun — fungsi, lambda, maupun functor — dan menggantinya saat program berjalan. std::function<R(Args...)> dari header <functional> (sejak C++11) menjawab kebutuhan itu lewat type erasure, mekanisme yang di Chapter Generic Programming Patterns kita singgung secara konseptual. Chapter ini membahas cara menyimpan dan mengganti target, pemeriksaan keadaan kosong beserta biaya pembungkusnya, alasan lambda lebih disarankan daripada std::bind, pola callback registry, dan penerusnya di C++23: std::move_only_function.

Callable Polimorfik dalam Satu Wadah #

Tipe std::function<int(int, int)> dibaca: “wadah callable yang dipanggil dengan dua int dan mengembalikan int”. Isi wadahnya disebut target, dan target boleh apa pun yang bisa disalin (copy constructible) serta cocok dengan tanda tangan itu — function pointer, lambda, functor, bahkan pointer ke member function. Assignment mengganti target; dua wadah dengan tipe sama bisa diisi callable yang berbeda pula. Di balik layar, compiler membentuk lapisan adaptor yang menyamarkan tipe asli callable ke satu antarmuka panggilan bersama: ukuran wadah selalu tetap, sedangkan target sendiri disimpan di buffer internal atau di heap — tergantung implementasi dan ukuran callable-nya.

cpp
#include <functional>  // std::function
#include <iostream>    // std::cout
#include <string>      // std::string

// Callable pertama: fungsi bebas biasa
int tambah(int a, int b) { return a + b; }

// Callable kedua: functor dari Chapter Function Objects
struct Pengali {
    int faktor;
    int operator()(int a, int b) const { return a * b * faktor; }
};

// Parameter generik (template): menerima callable APA PUN tanpa pembungkus
template <typename F>
void terapkan(const std::string& label, F operasi) {
    std::cout << label << operasi(6, 7) << "\n";
}

int main() {
    std::function<int(int, int)> operasi;  // lahir kosong (empty)

    operasi = tambah;  // 1) function pointer
    std::cout << "function pointer: " << operasi(6, 7) << "\n";

    operasi = [](int a, int b) { return a - b; };  // 2) lambda
    std::cout << "lambda          : " << operasi(6, 7) << "\n";

    operasi = Pengali{3};  // 3) functor
    std::cout << "functor         : " << operasi(6, 7) << "\n";

    // Parameter template menerima keduanya tanpa biaya pembungkus
    terapkan("parameter lambda: ", [](int a, int b) { return a + b; });
    terapkan("parameter wrapper: ", operasi);
    return 0;
}

Output yang Dihasilkan:

text
function pointer: 13
lambda          : -1
functor         : 126
parameter lambda: 13
parameter wrapper: 126

Mari kita bedah:

  1. Baris 5-6 (int tambah(int a, int b)): fungsi bebas biasa. Saat ditulis operasi = tambah, namanya meluruh menjadi function pointer — bentuk callable paling dasar.
  2. Baris 8-12 (struct Pengali): functor dari Chapter Function Objects — struct dengan operator(), lengkap dengan state faktor. Tipe ini berbeda total dari function pointer, tetapi muat di wadah yang sama.
  3. Baris 14-18 (template <typename F> void terapkan(...)): parameter generik menerima callable apa pun tanpa pembungkus; tipe aslinya tetap diketahui compiler sehingga pemanggilan bisa di-inline.
  4. Baris 21 (std::function<int(int, int)> operasi;): wadah lahir tanpa target (empty); baris 23 mengisinya untuk pertama kali.
  5. Baris 23-30 (tiga assignment): target berganti tiga kali — tambah, lambda pengurang, lalu Pengali{3} — sementara tipe wadah tidak pernah berubah. Hasil pemanggilan mengikuti target terakhir: 13, -1, lalu 126.
  6. Baris 32-34: dua pemanggilan terapkan dengan callable berbeda. Versi lambda langsung dipanggil tanpa biaya tambahan; versi operasi melewati pembungkus std::function yang sudah berisi Pengali.

Kapan kita perlu std::function, dan kapan cukup parameter template? Untuk parameter fungsi, parameter template (atau concept pada Section Generic Programming dan Templates) adalah pilihan pertama karena tanpa biaya. std::function layak dipakai ketika callable harus disimpan — anggota class, elemen std::vector, variabel yang nilainya berganti — atau dikirim melewati batas yang tidak boleh tahu tipenya.

Cek Keadaan Kosong dan Biaya di Baliknya #

std::function yang dibangun tanpa target disebut empty. Operator bool melaporkan ada atau tidaknya target sehingga kita bisa memeriksa sebelum memanggil; bila tetap dipanggil, wadah kosong melempar std::bad_function_call. Perilaku ini berbeda dari function pointer yang boleh nullptr namun undefined behavior bila dipanggil — di sini kesalahan terdeteksi dengan exception yang jelas (try/catch kita kenal sejak Chapter std::vector dan dibahas tuntas pada Section Error Handling).

cpp
#include <functional>  // std::function, std::bad_function_call
#include <iostream>    // std::cout

int main() {
    std::cout << std::boolalpha;

    std::function<int(int)> kuadrat = [](int x) { return x * x; };
    std::cout << "kuadrat terisi : " << static_cast<bool>(kuadrat) << "\n";
    std::cout << "kuadrat(9)     : " << kuadrat(9) << "\n";

    std::function<int(int)> kosong;  // default constructor: tanpa target
    std::cout << "kosong terisi  : " << static_cast<bool>(kosong) << "\n";

    try {
        kosong(5);  // memanggil std::function yang kosong
    } catch (const std::bad_function_call& e) {
        std::cout << "tertangkap     : " << e.what() << "\n";
    }

    kosong = kuadrat;  // diisi ulang: kini punya target
    std::cout << "kosong(8)      : " << kosong(8) << "\n";
    return 0;
}

Output yang Dihasilkan:

text
kuadrat terisi : true
kuadrat(9)     : 81
kosong terisi  : false
tertangkap     : bad_function_call
kosong(8)      : 64

Mari kita bedah:

  1. Baris 7-9: kuadrat diisi lambda dan static_cast<bool>(kuadrat) memakai operator bool milik wadah — hasilnya true karena ada target; panggilan kuadrat(9) menghasilkan 81.
  2. Baris 11-12: wadah kosong dibangun dengan default constructor, tanpa target; operator bool melaporkan false.
  3. Baris 14-18: kosong(5) memanggil wadah tanpa target, sehingga std::bad_function_call dilempar; blok catch menangkapnya dan mencetak teks bawaan libstdc++: bad_function_call.
  4. Baris 20-21: assignment ulang mengisi target; panggilan berikutnya berjalan normal dan menghasilkan 64.

Biaya kemudahannya perlu diketahui. Panggilan lewat std::function melewati satu lapis indireksi (pemanggilan fungsi virtual di bagian internal), sehingga compiler jarang bisa meng-inline-nya seperti pemanggilan lambda langsung. Penyalinan wadah menyalin targetnya — dan bila target tersimpan di heap, penyalinan itu mengalokasi memori baru. Ukuran wadah pun lebih besar daripada callable mentahnya karena membawa mesin adaptor di dalamnya.

Mengapa Lambda Lebih Disarankan daripada std::bind #

Di pustaka standar ada juga std::bind (C++11): alat partial application yang menyematkan sebagian argumen fungsi dan menyisakan “lubang” bertanda tempat (_1, _2) untuk argumen sisanya. Dalam course ini std::bind sengaja tidak pernah diajarkan, dan alasannya bukan karena fitur itu salah, melainkan karena lambda mencapai hasil yang sama dengan kode yang lebih jelas. Logika lambda terbaca sebagai kode biasa tanpa placeholder yang harus dilacak, urutan argumen tidak bisa tersusun ulang diam-diam, capture eksplisit ([=], [&]) membuat sumber data terlihat, dan compiler lebih mudah meng-inline-lambda sehingga hasilnya biasanya lebih cepat. Sejak C++20 standar juga menyediakan bind_front untuk kebutuhan penyematan argumen di awal yang tidak butuh penataan ulang. Perbandingan berikut memakai std::bind satu kali — hanya agar bentuknya bisa kita nilai sendiri:

cpp
#include <functional>  // std::bind, std::placeholders::_1
#include <iostream>    // std::cout

int bagi(int pembilang, int penyebut) {
    return pembilang / penyebut;
}

int main() {
    using std::placeholders::_1;

    // Gaya std::bind: 100 disematkan, _1 adalah "lubang" argumen ke-1
    auto seperseratusBind = std::bind(bagi, 100, _1);

    // Gaya lambda: niat yang sama, alurnya terbaca seperti kode biasa
    auto seperseratusLambda = [](int penyebut) { return bagi(100, penyebut); };

    std::cout << "std::bind : " << seperseratusBind(4) << "\n";
    std::cout << "lambda    : " << seperseratusLambda(4) << "\n";
    return 0;
}

Output yang Dihasilkan:

text
std::bind : 25
lambda    : 25

Mari kita bedah:

  1. Baris 12 (std::bind(bagi, 100, _1)): nilai 100 disematkan sebagai argumen pertama, dan _1 menandai lubang argumen pertama hasil bind. Membaca seperseratusBind(4) mengharuskan kita mengingat ulang aturan placeholder-nya.
  2. Baris 15: lambda mencapai niat yang sama — “bagi 100 dengan penyebut tertentu” — tanpa perantara; tubuh fungsi terbaca langsung.
  3. Baris 17-18: keduanya menghasilkan 25 untuk argumen 4, membuktikan std::bind dan lambda setara secara perilaku untuk kasus ini.

Pola Callback Registry #

Gabungan std::vector + std::function menghasilkan pola yang paling sering kita jumpai di dunia nyata: callback registry — papan tempat pihak lain mendaftarkan reaksi, lalu program memanggil semuanya ketika suatu peristiwa terjadi. Setiap slot menyimpan callable dengan tipe asli yang berbeda-beda, tetapi tipe slotnya seragam: std::function<void(const std::string&)>.

cpp
#include <functional>  // std::function
#include <iostream>    // std::cout
#include <string>      // std::string
#include <vector>      // std::vector

// Registry callback: simpan banyak reaksi, panggil semua saat sinyal datang
class PapanPengumuman {
public:
    void daftar(std::function<void(const std::string&)> reaksi) {
        reaksi_.push_back(reaksi);  // std::function bisa disalin, aman disimpan
    }

    void umumkan(const std::string& pesan) const {
        for (const std::function<void(const std::string&)>& r : reaksi_) {
            r(pesan);  // panggil satu per satu, urutannya sesuai pendaftaran
        }
    }

private:
    std::vector<std::function<void(const std::string&)>> reaksi_;
};

int main() {
    PapanPengumuman papan;

    papan.daftar([](const std::string& pesan) {
        std::cout << "[log] " << pesan << "\n";
    });
    papan.daftar([](const std::string& pesan) {
        std::cout << "[kirim] panjang: " << pesan.size() << "\n";
    });
    papan.daftar([](const std::string& pesan) {
        std::cout << "[tampil] " << pesan << "\n";
    });

    papan.umumkan("Server mulai");
    papan.umumkan("Data tersimpan");
    return 0;
}

Output yang Dihasilkan:

text
[log] Server mulai
[kirim] panjang: 12
[tampil] Server mulai
[log] Data tersimpan
[kirim] panjang: 14
[tampil] Data tersimpan

Mari kita bedah:

  1. Baris 9-11 (daftar): parameter bertipe std::function<void(const std::string&)> menerima lambda apa pun yang cocok dengan tanda tangan itu, lalu push_back menyimpan salinannya ke papan.
  2. Baris 13-17 (umumkan): loop menjalankan setiap slot dengan r(pesan); urutan pemanggilan mengikuti urutan pendaftaran, dan umumkan bertanda const karena hanya membaca papan.
  3. Baris 19-21 (reaksi_): anggota std::vector<std::function<void(const std::string&)>> — satu tipe slot untuk banyak reaksi; inilah alasan type erasure diperlukan.
  4. Baris 26-34: tiga lambda didaftarkan; masing-masing mencetak dengan format berbeda, tanpa satu pun di antaranya saling mengenal.
  5. Baris 36-37: dua umumkan memicu tiga callback per pesan — enam baris keluaran berurutan sesuai pendaftaran.

std::move_only_function: Wadah untuk Callable yang Tak Bisa Disalin #

Satu batas std::function perlu diingat: targetnya wajib bisa disalin (copy constructible). Lambda yang menangkap std::unique_ptr — pointer yang hanya boleh dipindah, dibahas pada Section Move Semantics dan Value Categories — tidak muat, karena menyalin lambda itu berarti menyalin unique_ptr yang dilarang. C++23 memperkenalkan std::move_only_function di header <functional>: pembungkus polimorfik dengan ide yang sama, tetapi wadahnya memenuhi syarat MoveConstructible dan MoveAssignable saja — penyalinan tidak tersedia. Dengan aturan itu, lambda move-only kini punya rumah yang sah.

Dua perbedaan perilaku perlu dicatat sebelum berpindah. Pertama, tanda tangan move_only_function bisa menyertakan qualifier (const, &, &&, noexcept) sehingga cara wadah dipanggil ikut menjadi bagian tipenya. Kedua, memanggil wadah move_only_function yang kosong adalah undefined behavior — bukan exception std::bad_function_call seperti pada std::function, sehingga pemeriksaan operator bool sebelum memanggil menjadi tanggung jawab kita. GCC 14 yang kita pakai sudah mendukungnya; pilih std::function bila wadah perlu disalin, dan std::move_only_function bila targetnya move-only.

Wadah callable melengkapi kotak alat utilitas standar: fungsi kini bisa mengalir sebagai data. Chapter berikutnya berpindah ke pengolahan teks berpola — std::regex, bahasa kecil untuk mencari, memvalidasi, dan mengganti teks di dalam string.

Rujukan resmi: cppreference — std::function , cppreference — std::bad_function_call , cppreference — std::move_only_function .