Macro function-like adalah macro yang menerima parameter—secara tampilan menyerupai fungsi, tetapi secara mekanisme tetap berupa substitusi teks oleh preprocessor. Fasilitas ini berguna untuk operasi kecil yang ingin kita hindari overhead pemanggilan fungsi, atau untuk operasi generik yang harus bekerja pada banyak tipe data (mis. MAX antara dua int, dua double, atau dua char). Namun, karena tidak ada pemeriksaan tipe dan tidak ada evaluation order seperti fungsi biasa, macro berparameter sarat jebakan. Pada chapter ini kita akan mempelajari cara mendefinisikan macro berparameter, kewajiban menambahkan tanda kurung di sekitar parameter, dan bahaya side effect ganda saat argumen mengandung operator ++ atau pemanggilan fungsi.
Definisi dan Sintaks Dasar #
Macro function-like didefinisikan dengan menempatkan tanda kurung (...) tepat setelah nama macro—tanpa spasi. Daftar parameter di dalam kurung adalah nama formal yang akan diganti dengan argumen aktual saat macro dipakai. Standar ISO/IEC 9899 §6.10.3 menetapkan aturan ini: jika ada spasi antara nama macro dan kurung buka, macro tersebut menjadi object-like dengan nilai yang mengandung kurung.
#include <stdio.h>
/* Macro function-like dengan satu parameter */
#define KUADRAT(x) ((x) * (x))
/* Macro function-like dengan dua parameter */
#define MAX(a, b) ((a) > (b) ? (a) : (b))
/* Macro dengan tanda kurung ekstra untuk mencegah precedence bug */
#define KALI_DAN_TAMBAH(a, b, c) (((a) * (b)) + (c))
int main(void) {
int n = 5;
printf("KUADRAT(5) = %d\n", KUADRAT(5));
printf("KUADRAT(n+1) = %d\n", KUADRAT(n + 1)); /* Aman: (n+1) dikelilingi kurung */
printf("MAX(3, 7) = %d\n", MAX(3, 7));
printf("MAX(3.5, 2) = %.2f\n", MAX(3.5, 2)); /* Generic untuk tipe apapun */
printf("KALI_DAN_TAMBAH(2,3,4) = %d\n", KALI_DAN_TAMBAH(2, 3, 4));
return 0;
}Output yang Dihasilkan:
KUADRAT(5) = 25
KUADRAT(n+1) = 36
MAX(3, 7) = 7
MAX(3.5, 2) = 3.50
KALI_DAN_TAMBAH(2,3,4) = 10Perhatikan bahwa MAX(3.5, 2) mengembalikan 3.50—bekerja pada tipe double tanpa perlu template seperti di C++. Ini adalah salah satu daya tarik utama macro berparameter: sifat generik tanpa overhead.
Kenapa Tanda Kurung Wajib di Sekitar Parameter? #
Mari kita lihat apa yang terjadi jika kita melupakan tanda kurung. Misalkan kita mendefinisikan #define KUADRAT_BURUK(x) x * x (tanpa tanda kurung di sekitar x). Saat macro dipanggil dengan KUADRAT_BURUK(n + 1), preprocessor menghasilkan n + 1 * n + 1. Karena operator * memiliki precedence lebih tinggi dari +, ekspresi dievaluasi sebagai n + (1 * n) + 1 = n + n + 1, yang sama sekali berbeda dari (n+1) * (n+1).
Mari kita telaah baris-baris macro KUADRAT(x) yang benar:
- Baris
#define KUADRAT(x) ((x) * (x)): tanda kurung terluar((x) * (x))melindungi seluruh ekspresi agar tidak tercampur dengan operator di sekitarnya saat macro dipakai dalam ekspresi yang lebih besar. Tanda kurung dalam(x)melindungi parameterxsendiri jika argumen adalah ekspresi sepertin + 1. - Baris
#define MAX(a, b) ((a) > (b) ? (a) : (b)): macro ini menggunakan operator ternary?:. Tanpa tanda kurung luar, pemanggilanif (MAX(x, y) > 0)akan menghasilkan kode ambigu yang dapat di-parses secara salah. - Baris
#define KALI_DAN_TAMBAH(a, b, c) (((a) * (b)) + (c)): macro tiga parameter. Setiap parameter dikelilingi tanda kurung, dan seluruh sub-ekspresi(a) * (b)juga dikelilingi tanda kurung agar urutan evaluasi sesuai harapan.
Jebakan Side Effect Ganda #
Karena macro adalah substitusi teks, argumen yang dievaluasi dapat muncul lebih dari sekali pada hasil ekspansi. Jika argumen mengandung side effect seperti i++ atau pemanggilan fungsi yang mengubah state, side effect tersebut akan dipicu untuk setiap kemunculan argumen pada badan macro. Inilah perbedaan paling fundamental antara macro dan fungsi biasa.
#include <stdio.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main(void) {
int i = 1, j = 5;
/* BUG side effect ganda: i++ dievaluasi 2 kali */
int m = MAX(i++, j++);
printf("m = %d, i = %d, j = %d\n", m, i, j);
return 0;
}Output yang Dihasilkan:
m = 6, i = 2, j = 7Setelah preprocessing, baris int m = MAX(i++, j++); menjadi:
int m = ((i++) > (j++) ? (i++) : (j++));Mari kita telaah apa yang terjadi pada baris di atas:
- Tahap 1 (evaluasi kondisi): ekspresi
(i++) > (j++)dievaluasi.ibernilai1, lalu naik menjadi2.jbernilai5, lalu naik menjadi6. Nilai perbandingan:1 > 5adalahfalse. - Tahap 2 (cabang false dieksekusi): karena kondisi
false, operator ternary memilih cabang kanan(j++). Pada titik inijsudah bernilai6(akibat evaluasi kondisi), jadij++mengembalikan6dan menaikkanjmenjadi7. Nilai6inilah yang dimasukkan kem. - Tahap 3 (cabang true tidak dieksekusi):
(i++)pada cabang true tidak dievaluasi sama sekali. Inilah alasanihanya naik sekali menjadi2, bukan menjadi3.
Jika kita menulis MAX sebagai fungsi biasa int max(int a, int b), argumen i++ dan j++ akan dievaluasi sekali sebelum dipakai sebagai parameter. Hasil yang diharapkan—m=5, i=2, j=6—jauh berbeda dari hasil macro yang sebenarnya m=6, i=2, j=7. Perbedaan ini sering menjadi sumber bug yang sangat sulit dilacak.