Chapter sebelumnya memperkenalkan parameter pack lewat pola rekursi. Sekarang kita perhalus sisi yang paling sering dipakai di kode nyata: pack expansion — bagaimana compiler “membuka” pack dan menyalin pola yang kita tulis untuk setiap elemennya. Pola yang diperluas tidak harus sekadar nama variabel; bisa berupa pemanggilan fungsi, ekspresi, atau kombinasi keduanya. Kemampuan inilah yang dipakai emplace_back, std::format, dan berbagai fungsi standard library untuk meneruskan argumen bebas jumlahnya ke tujuan akhirnya.

Expansion Pattern: Apa yang Sebenarnya Diperluas #

Aturan dasarnya satu kalimat: tulis pola untuk satu elemen, lalu tempel ... di kanannya — compiler mengulangi pola itu untuk setiap elemen pack, dipisah koma. Beberapa bentuk yang paling sering muncul:

PolaHasil untuk pack (10, "dua", 3.5)
args...10, "dua", 3.5 — diteruskan apa adanya
prosesSatu(args)...prosesSatu(10), prosesSatu("dua"), prosesSatu(3.5)
(args, 0)...(10, 0), ("dua", 0), (3.5, 0) — hasil tiap elemen 0
std::forward<Args>(args)...setiap elemen diteruskan dengan menjaga kategori nilainya

Contoh di bawah memakai pola pemanggilan fungsi: satu wrapper menerima pack, lalu menyalurkan setiap elemen ke prosesSatu tanpa menulis rekursi.

cpp
#include <iostream>
#include <string>

// Fungsi target: menerima satu nilai
template <typename T>
void prosesSatu(const T& nilai) {
    std::cout << "proses: " << nilai << "\n";
}

// Wrapper: memforward pack ke fungsi target, satu panggilan per elemen
template <typename... Args>
void prosesSemua(const Args&... args) {
    // Expansion pattern: prosesSatu(args) diulang untuk setiap elemen pack
    [[maybe_unused]] int penanda[] = { (prosesSatu(args), 0)... };
}

int main() {
    prosesSemua(10, "dua puluh", 3.5);

    return 0;
}

Output yang Dihasilkan:

text
proses: 10
proses: dua puluh
proses: 3.5

Bedah baris demi baris:

  1. Baris 12 (const Args&... args): pack di daftar parameter fungsi. Untuk pemanggilan pada baris 18, Args diduksi menjadi {int, const char*, double} dan args berisi tiga rujukan konstan.
  2. Baris 14 ((prosesSatu(args), 0)...): polanya adalah ekspresi koma — panggil prosesSatu untuk satu elemen, lalu hasilkan 0 sebagai nilainya. Dengan ... di ujung, compiler menulis ulang seluruh pola menjadi { (prosesSatu(10), 0), (prosesSatu("dua puluh"), 0), (prosesSatu(3.5), 0) }.
  3. Baris 14 juga menjelaskan wadahnya: ekspresi pola butuh context yang menerima daftar berpisah koma; daftar inisialisasi array adalah context klasiknya. Nilainya sendiri tidak dipakai — [[maybe_unused]] menenangkan compiler.
  4. Urutan terjamin: di dalam daftar inisialisasi kurung kurawal, setiap elemen dievaluasi kiri-ke-kanan, sehingga tiga baris proses: muncul persis dalam urutan argumen.

Memforward Pack ke Fungsi Lain #

prosesSemua di atas adalah contoh forwarding: fungsi perantara yang tidak memproses apa pun sendiri, hanya meneruskan isi pack ke fungsi lain. Bentuk paling sederhananya f(args)...; bentuk paling lengkapnya f(std::forward<Args>(args)...) — versi yang menjaga sifat rujukan setiap argumen, dikenal sebagai perfect forwarding. Kita tinggalkan detailnya ke Section Move Semantics dan Value Categories; untuk sekarang cukup pegang bentuknya, karena signature emplace_back yang segera kita bongkar persis memakai pola itu.

Perlu jujur diakui: trik array int penanda[] adalah warisan pra-C++17 yang masih sah, tetapi terlihat rancu. C++17 memberi ganti yang bersih — fold expression — dan seluruh pola koma ini bisa dipangkas jadi satu baris. Itu judul chapter berikutnya.

Contoh Nyata: emplace_back #

emplace_back adalah pengguna parameter pack paling terkenal di standard library. Signature aslinya berbunyi:

cpp
template <class... Args>
reference emplace_back(Args&&... args);

Bacaannya: terima argumen apa saja, teruskan semuanya ke constructor tipe elemen, dan bangun object langsung di dalam memori vector — tanpa object sementara yang kemudian disalin. Program berikut membuktikannya dengan jejak constructor:

cpp
#include <iostream>
#include <string>
#include <vector>

class Siswa {
public:
    Siswa(std::string nama, int umur) : nama_{std::move(nama)}, umur_{umur} {
        std::cout << "constructor Siswa untuk " << nama_ << "\n";
    }

    const std::string& nama() const { return nama_; }
    int umur() const { return umur_; }

private:
    std::string nama_;
    int umur_;
};

int main() {
    std::vector<Siswa> daftar;
    daftar.reserve(2);

    // emplace_back meneruskan seluruh argumen ke constructor Siswa
    daftar.emplace_back("Budi", 17);
    daftar.emplace_back("Siti", 16);

    for (const Siswa& s : daftar) {
        std::cout << s.nama() << " (" << s.umur() << " tahun)\n";
    }

    return 0;
}

Output yang Dihasilkan:

text
constructor Siswa untuk Budi
constructor Siswa untuk Siti
Budi (17 tahun)
Siti (16 tahun)

Yang patut diperhatikan:

  1. Baris 7-9: constructor mencetak jejak setiap kali dijalankan. Hanya dua baris “constructor Siswa” muncul — satu per emplace_back — membuktikan tidak ada penyalinan object sementara.
  2. Baris 24-25: emplace_back("Budi", 17) mengirim dua argumen dengan tipe berbeda; parameter pack di dalam vector yang menerimanya. push_back setara harus diisi satu object Siswa yang sudah jadi, sedangkan emplace_back mengirim bahan bakunya.
  3. Baris 7 lagi: std::move(nama) memindahkan string ke data member tanpa menyalin isinya — detail yang dibedah di Section Move Semantics.
  4. Baris 21: reserve(2) memesan kapasitas lebih dulu agar tidak ada alokasi ulang yang mengejutkan; kebiasaan baik saat jumlah elemen sudah diketahui.

Dengan pack dan pola perluasannya terkuasai, potongan yang tersisa kecil namun berdampak besar: bentuk penjumlahan dan pemanggilan berantai yang dulu butuh rekursi kini jadi satu ekspresi. Chapter Fold Expressions menutup trilogi variadic ini.

Referensi resmi: cppreference — Parameter pack dan cppreference — vector::emplace_back .