Fungsi variadik (variadic function) menerima jumlah argumen yang tidak tetap, dan keluarga printf serta scanf adalah contoh yang paling dikenal. Topik ini membahas header stdarg.h, yang menyediakan mekanisme standar untuk menulis fungsi semacam itu sendiri. Pembahasannya mencakup anatomi deklarasi, makro va_start, va_arg, dan va_end yang membaca argumen satu per satu, serta pola penerusan argumen ke vprintf. Pemahaman mekanisme ini sekaligus menjelaskan aturan main yang wajib dipatuhi pemanggil printf, karena pemeriksaan tipe pada argumen variadik nyaris tidak ada.
Anatomi fungsi variadik#
Deklarasi fungsi variadik ditandai tiga titik (…) sebagai parameter terakhir, dan minimal satu parameter bernama harus mendahuluinya. Parameter bernama itulah yang menjadi patokan, karena makro va_start membutuhkannya untuk menemukan posisi argumen pertama yang bersifat variabel. Argumen-argumen variabel itu kemudian dibaca lewat objek bertipe va_list, yang perilakunya mirip kursor penelusuran yang bergerak dari satu argumen ke argumen berikutnya.
Pola bacaannya selalu sama, yaitu va_start menyiapkan kursor, va_arg(daftar, tipe) mengambil satu argumen dan menggeser kursor, lalu va_end menutup pembacaan. Karena bahasa C tidak menghitung jumlah argumen yang dikirim, fungsinya perlu tahu kapan berhenti, dan dua pola yang umum adalah parameter penghitung sebagaimana contoh berikut atau nilai penanda (sentinel) yang disepakati:
#include <stdarg.h>
#include <stdio.h>
int jumlah(int banyak, ...)
{
int total = 0;
va_list argumen;
va_start(argumen, banyak);
for (int i = 0; i < banyak; i = i + 1) {
total = total + va_arg(argumen, int);
}
va_end(argumen);
return total;
}
double rata_rata(int banyak, ...)
{
va_list argumen;
int total = 0;
va_start(argumen, banyak);
for (int i = 0; i < banyak; i = i + 1) {
total = total + va_arg(argumen, int);
}
va_end(argumen);
return (double)total / banyak;
}
int main(void)
{
printf("jumlah(3, 10, 20, 30) = %d\n", jumlah(3, 10, 20, 30));
printf("jumlah(5, 2, 4, 6, 8, 10) = %d\n", jumlah(5, 2, 4, 6, 8, 10));
printf("rata_rata(4, 70, 85, 90, 65) = %.1f\n", rata_rata(4, 70, 85, 90, 65));
return 0;
}jumlah(3, 10, 20, 30) = 60
jumlah(5, 2, 4, 6, 8, 10) = 30
rata_rata(4, 70, 85, 90, 65) = 77.5Kedua fungsi menerima jumlah argumen yang berbeda pada setiap pemanggilan, dan parameternya selalu diawali bilangan penghitung yang menyatakan berapa nilai variabel mengikutinya. Pola penghitung sederhana dan aman untuk nilai bertipe sama, sedangkan printf memilih pola lain, yaitu membedah string pemformat untuk menentukan tipe serta jumlah argumen yang harus dibaca berikutnya.
Aturan membaca argumen#
Makro va_arg(tipe) menyalin argumen berikutnya dengan tipe yang disebutkan, dan tipe tersebut wajib cocok dengan tipe yang dikirim pemanggil. Ketidakcocokan, misalnya membaca double padahal pemanggil mengirim int, menghasilkan perilaku tak terdefinisi tanpa peringatan saat kompilasi, karena deklarasi fungsi variadik tidak membawa informasi tipe untuk argumen variabelnya. Alat seperti gcc dengan flag -Wformat hanya bisa memeriksa fungsi yang pola argumennya dikenali, sehingga fungsi buatan sendiri tidak menikmati pemeriksaan itu.
Konversi bawaan juga berlaku pada argumen variadik, yaitu char dan short dipromosikan menjadi int, sedangkan float dipromosikan menjadi double. Konsekuensinya, va_arg hanya boleh meminta tipe setelah promosi tersebut, misalnya int atau double, karena meminta short atau float pada argumen variadik adalah kesalahan tipe. Membaca lebih banyak argumen daripada yang dikirim juga merupakan perilaku tak terdefinisi, sehingga jumlah argumen harus dikelola dengan penghitung atau sentinel secara disiplin.
Meneruskan argumen dengan vprintf#
Kasus yang sering muncul adalah menulis pembungkus (wrapper) printf sendiri, misalnya untuk mencatat pesan dengan awalan tertentu. Fungsi pembungkus tidak dapat meneruskan argumen variadik secara langsung, karena tiga titik tidak bisa dilempar ke panggilan lain sebagaimana argumen biasa. Header stdarg.h menjawab kebutuhan itu bersama keluarga vprintf, yaitu vprintf, vfprintf, dan vsnprintf yang menerima va_list yang sudah siap:
#include <stdarg.h>
#include <stdio.h>
int catat(const char *format, ...)
{
va_list argumen;
int panjang;
va_start(argumen, format);
panjang = vprintf(format, argumen);
va_end(argumen);
return panjang;
}
int main(void)
{
int tercetak = catat("Suhu %.1f derajat, tekanan %d kPa.\n", 27.5, 1013);
printf("Karakter tercetak: %d\n", tercetak);
return 0;
}Suhu 27.5 derajat, tekanan 1013 kPa.
Karakter tercetak: 37Fungsi catat meneruskan seluruh argumen variadiknya ke vprintf lewat va_list, dan nilai kembali vprintf berupa jumlah karakter yang tercetak. Pola ini dipakai hampir semua pustaka pencatat (logging) berbasis C, karena pemformatan lengkap dengan penentu printf tetap tersedia tanpa perlu menulis ulang mesin pemformatnya. Apabila pesannya perlu ditampung dulu, vsnprintf menggantikan vprintf dengan menambahkan argumen buffer beserta ukurannya. Nilai kembali keluarga vprintf berupa jumlah karakter yang seharusnya tercetak, bukan jumlah yang benar-benar tertulis apabila buffer terpotong. Karena itu pola yang aman dengan vsnprintf adalah membandingkan nilai kembinya dengan ukuran buffer, lalu mengulang pemformatan pada buffer yang lebih besar apabila keluarannya ternyata tidak muat.
va_copy untuk menyalin kursor#
Makro va_copy menyalin keadaan kursor va_list, sehingga satu rangkaian argumen dapat ditelusuri lebih dari satu kali tanpa memanggil va_start berulang. Salinan yang sudah selesai dibaca wajib ditutup va_end-nya sendiri, sebagaimana kursor aslinya:
// tidak-dijalankan
va_list pertama, kedua;
va_start(pertama, format);
va_copy(kedua, pertama); /* salinan menunjuk argumen pertama */
total = jumlahkan_semua(pertama); /* kursor pertama habis dipakai */
panjang = hitung_cetak(kedua); /* kursor kedua masih dari awal */
va_end(kedua);
va_end(pertama);Pemakaian yang lazim adalah menghitung ukuran keluaran pada salinan, misalnya dengan vsnprintf ke buffer kosong, lalu membaca argumen aslinya untuk eksekusi yang sesungguhnya. Kebutuhan salinan juga muncul pada fungsi yang ingin memeriksa argumennya lebih dahulu sebelum memutuskan cara memprosesnya, misalnya menghitung panjang total argumen teks sebelum memesan buffer dengan ukuran yang pas. Tanpa va_copy, pemeriksaan awal sudah menggerakkan kursor ke ujung, sehingga pembacaan kedua harus dimulai lagi dari va_start dan seluruh argumen harus tetap tersedia. Salinan membuat kedua tahap itu berjalan atas data yang sama tanpa menambah pekerjaan pemanggilnya.
Mekanisme ini juga punya batas. va_list tidak dapat dioper ke fungsi yang menyimpannya untuk dipakai setelah fungsi pembuatnya kembali, karena masa sah kursor terikat pada aktivasi fungsinya.
Kesalahan umum#
Empat kesalahan berikut paling sering muncul pada fungsi variadik buatan sendiri.
- Membaca dengan tipe yang tidak cocok. Meminta double pada argumen yang dikirim sebagai int menghasilkan nilai acak atau kegagalan mendadak, tanpa peringatan kompiler. Pastikan kontrak antara pemanggil dan fungsi variadik tertulis dengan jelas, biasanya lewat string pemformat atau parameter tipe.
- Membaca melewati argumen terakhir. Tanpa penghitung atau sentinel yang konsisten, va_arg terus membaca sampah dari tumpukan pemanggilan. Tetapkan pola penghentian sejak desain dan patuhinya pada setiap pemanggilan.
- Melupakan va_end. Ketiadaan va_end menyebabkan kursor tidak dirapikan dan perilakunya tidak terdefinisi pada beberapa platform. Jadikan va_end bagian tetap dari pola penulisannya, bukan pelengkap opsional.
- Meminta float atau short dari va_arg. Kedua tipe itu sudah dipromosikan menjadi double dan int pada argumen variadik, sehingga permintaannya salah tipe. Minta double dan int, lalu konversikan kembali bila perlu.
Ringkasan#
- Fungsi variadik diawali minimal satu parameter bernama, dan tiga titik menyatakan argumen variabel yang dibaca lewat va_list.
- Makro va_start menyiapkan kursor setelah parameter bernama terakhir, va_arg mengambil satu argumen sesuai tipe yang diminta, dan va_end menutup pembacaan.
- Tipe argumen variadik mengalami promosi, yaitu char dan short menjadi int serta float menjadi double, dan ketidakcocokan tipe pembacaan adalah perilaku tak terdefinisi.
- Keluarga vprintf menerima va_list sehingga fungsi pembungkus printf dapat ditulis, sedangkan va_copy menyalin kursor untuk menelusuri argumen lebih dari satu kali.
Referensi#
- stdarg(3) — Linux man-pages project — Dokumentasi makro va_start, va_arg, va_end, dan va_copy beserta aturannya.
- va_arg(3) — Linux man-pages project — Rincian pembacaan argumen variadik dan penjelasan aturan promosi tipe.
- Variadic functions — cppreference.com — Ikhtisar header stdarg.h beserta halaman detail setiap makronya.
- vprintf(3) — Linux man-pages project — Keluarga fungsi printf yang menerima va_list untuk pola penerusan argumen.