Variadic template dan fold expression

Parameter pack dan cara kerja API bergaya printf.

Daftar isi

Topik ini membahas variadic template, yaitu template yang menerima jumlah argumen yang tidak ditetapkan sebelumnya, beserta fold expression dari C++17 yang menyederhanakan penulisannya. Pembahasan mencakup deklarasi parameter pack, penghitungan argumennya, pelipatan pack menjadi satu ekspresi, dan cara API bergaya printf bekerja di balik layar. Contohnya berupa satu program utuh yang memuat ketiga bentuk pemakaiannya.

Parameter pack#

Sebuah parameter pack dideklarasikan dengan menulis elipsis pada daftar parameter template, yaitu typename... Args. Parameter semacam ini menerima nol atau lebih argumen tipe sekaligus, dan setiap pemanggilan mengikat jumlah argumen yang berbeda tanpa overloading tambahan. Di dalam badan template, pack diperluas dengan operator elipsis yang menyalin pola ekspresi untuk setiap argumen, dan ukurannya dibaca dengan sizeof....

Program berikut memuat tiga template variadic, yaitu cetak yang melipat pack menjadi rangkaian keluaran, jumlahkan yang menjumlahkan seluruh argumennya, dan banyakArgumen yang menghitung banyaknya argumen.

cpp cetak-lipat.cpp
#include <iostream>
#include <string>

template <typename... Args>
void cetak(const Args&... args) {
    ((std::cout << args), ...);
    std::cout << "\n";
}

template <typename... Args>
double jumlahkan(const Args&... args) {
    return (args + ... + 0.0);
}

template <typename... Args>
std::size_t banyakArgumen(const Args&... args) {
    return sizeof...(args);
}

int main() {
    cetak("Nilai: ", 24, " dan ", 7.5);
    cetak("Nama: ", std::string("Ali"));
    cetak();
    std::cout << jumlahkan(1, 2, 3, 4) << "\n";
    std::cout << jumlahkan(0.5, 0.25) << "\n";
    std::cout << banyakArgumen(1, "dua", 3.0, 'd') << "\n";
    return 0;
}
text Keluaran
Nilai: 24 dan 7.5
Nama: Ali

10
0.75
4

Pemanggilan cetak("Nilai: ", 24, " dan ", 7.5) mengikat empat argumen, sedangkan cetak() tanpa argumen tetap sah karena pack boleh kosong. Baris kosong pada keluaran berasal dari pemanggilan itu, karena satu-satunya yang dijalankan adalah std::cout << "\n". banyakArgumen mengembalikan 4 untuk empat argumen campuran yang tipenya diturunkan satu per satu.

Fold expression#

Fold expression melipat pack menjadi satu ekspresi dengan operator yang ditentukan, dan fiturnya tersedia sejak C++17. Terdapat empat bentuk, yaitu (pack op ...), (... op pack), (pack op ... nilai), dan (nilai op ... op pack), dengan op sebagai operator biner seperti +, &&, atau koma. Bentuk yang menyertakan nilai awal disebut binary fold, sedangkan bentuk tanpanya disebut unary fold.

Pada program di atas, ((std::cout << args), ...) adalah unary right fold atas operator koma, yang menghasilkan rangkaian (arg1, (arg2, (arg3, ...))) yang setara dengan penulisan berantai. Operator koma mengeksekusi kiri ke kanan, sehingga setiap argumen tercetak berurutan dan urutannya pasti. Pemilihan koma bukan kebetulan, melainkan cara yang aman untuk menyusun aksi berurutan dalam satu ekspresi tanpa perulangan biasa, karena pack tidak dapat dilalui dengan for konvensional.

Pemanggilan jumlahkan memakai binary fold (args + ... + 0.0) dengan nilai awal 0.0. Nilai awal itu menjawab dua kebutuhan sekaligus, yaitu menjaga pemanggilan tanpa argumen tetap sah dan memaksa hasilnya bertipe double. Karena itu, jumlahkan(1, 2, 3, 4) tercetak sebagai 10, sedangkan jumlahkan(0.5, 0.25) tercetak sebagai 0.75.

Arah pelipatan memengaruhi hasil ketika operatornya tidak asosiatif, misalnya pengurangan. Bentuk (args - ...) melipat ke kanan sehingga menghasilkan 10 - (4 - 3), sedangkan (... - args) melipat ke kiri sehingga menghasilkan (10 - 4) - 3. Untuk operator + dan koma, kedua arah memberikan hasil yang sama, sehingga pilihan arahnya menjadi soal keterbacaan. Program pendek berikut memperlihatkan perbedaan tersebut pada operator pengurangan.

cpp
#include <iostream>

template <typename... Args>
auto kurangiKanan(Args... args) {
    return (args - ...);   // 10 - (4 - 3)
}

template <typename... Args>
auto kurangiKiri(Args... args) {
    return (... - args);   // (10 - 4) - 3
}

int main() {
    std::cout << kurangiKanan(10, 4, 3) << "\n";
    std::cout << kurangiKiri(10, 4, 3) << "\n";
    return 0;
}
text Keluaran
9
3

Pola rekursi sebelum fold expression#

Sebelum fold expression tersedia, pemrosesan pack dilakukan dengan rekursi antar template, yaitu fungsi menangani satu argumen lalu meneruskan sisanya ke pemanggilan berikutnya. Pola ini terdiri atas dua bagian, yaitu kasus dasar tanpa argumen tersisa dan kasus rekursif yang memotong satu argumen pada setiap langkah. Pack berisi empat argumen menghasilkan empat instans rekursif sebelum berhenti pada kasus dasarnya, sehingga jumlah instans bertumbuh sebanding dengan jumlah argumen yang dipakai.

cpp
#include <iostream>

// kasus dasar: dipanggil ketika pack sudah kosong
void cetakRekursif() {
    std::cout << "\n";
}

// kasus rekursif: proses satu argumen, lalu teruskan sisanya
template <typename T, typename... Sisa>
void cetakRekursif(const T& pertama, const Sisa&... sisanya) {
    std::cout << pertama;
    cetakRekursif(sisanya...);
}

int main() {
    cetakRekursif("Nilai: ", 24, " dan ", 7.5);
    return 0;
}
text Keluaran
Nilai: 24 dan 7.5

Keluarannya identik dengan cetak pada bagian parameter pack, tetapi mekanismenya berbeda karena setiap argumen melewati satu pemanggilan fungsi tersendiri. Kasus dasarnya dideklarasikan sebelum template rekursifnya agar sudah terlihat ketika compiler memeriksa template itu. Fold expression memangkas pola ini menjadi satu fungsi untuk sebagian besar kebutuhan, tetapi bentuk rekursinya tetap ditemui pada kode C++11 dan C++14 serta tetap dipakai ketika setiap langkah memerlukan pernyataan tersendiri yang panjang.

Cara kerja API bergaya printf#

API bergaya printf menerima format beserta sejumlah argumen yang jumlahnya baru diketahui saat pemanggilan. Versi lama dari gaya ini memakai mekanisme varargs C yang tidak memeriksa tipe argumennya, sehingga format dan argumen yang tidak cocok lolos kompilasi dan berakhir sebagai undefined behavior. Variadic template menjawab kelemahan itu, karena setiap argumen tetap membawa tipenya dan compiler menginstansiasi fungsi yang persis untuk kombinasi yang dipanggil.

Mekanisme pelakuannya terjadi saat kompilasi. Pemanggilan cetak("Nilai: ", 24, " dan ", 7.5) menghasilkan satu instans dengan empat parameter yang konkret, dan badannya sudah memuat rangkaian keluaran yang tersusun penuh, sehingga tidak ada perulangan maupun penguraian format saat program berjalan. Biayanya hanya bertambah sebesar kombinasi tipe yang benar-benar dipakai, sejalan dengan sifat instantiation pada function template.

Kesalahan umum#

Tiga kekeliruan berikut paling sering muncul saat menulis template variadic.

  • Memakai unary fold tanpa nilai awal pada pack yang mungkin kosong dengan operator selain &&, ||, dan koma. Bentuk semacam itu ditolak compiler karena tidak ada identitas yang mewakilinya. Perbaikannya menambahkan nilai awal, misalnya (args + ... + 0.0).
  • Meletakkan elipsis di tempat yang salah, misalnya menulis fold tanpa tanda kurung di sekelilingnya. Fold expression wajib diapit tanda kurung, dan posisi elipsis menentukan arah pelipatannya. Periksa keempat bentuknya dan tulis lengkap dengan tanda kurungnya.
  • Menulis perulangan biasa untuk melalui pack di dalam badan template. Pack bukan container yang hidup saat program berjalan, melainkan daftar argumen yang diperluas saat kompilasi, sehingga perulangan biasa tidak dapat menjangkaunya. Perluas pack dengan fold expression atau dengan rekursi antar template.

Ringkasan#

  • Parameter pack typename... Args menerima nol atau lebih argumen tipe, dan sizeof... menghitung banyaknya argumen pada satu instans.
  • Fold expression melipat pack menjadi satu ekspresi dengan empat bentuk penulisan, dan binary fold menyertakan nilai awal sekaligus tipe hasil.
  • API bergaya printf modern memakai variadic template agar setiap argumen tetap bertipe dan salah kecocokan tertangkap saat kompilasi, bukan menjadi undefined behavior.

Referensi#