Macro expansion (ekspansi macro) adalah proses substitusi teks yang dilakukan preprocessor saat menjumpai nama macro di kode sumber. Pemahaman tentang proses ini penting karena banyak bug halus di C berasal dari asumsi yang keliru tentang kapan dan berapa kali sebuah argumen macro di-expand. Pada chapter ini kita akan melihat dua operator khusus yang dipakai dalam ekspansi macro—# (stringifying) dan ## (token paste)—perbedaan antara ekspansi argumen yang dipakai secara langsung versus yang dilingkupi operator #/##, serta implikasinya pada side effect.
Aturan Ekspansi Dasar #
Saat preprocessor menjumpai pemanggilan macro KUADRAT(n + 1), preprocessor melakukan dua langkah. Pertama, argumen n + 1 di-expand (jika argumen itu sendiri berisi macro, macro tersebut di-expand lebih dulu). Kedua, argumen yang sudah di-expand menggantikan parameter formal di badan macro. Hasil akhir kemudian di-re-scan untuk kemungkinan ekspansi macro lebih lanjut—namun ekspansi rekursif pada macro yang sama dilarang untuk mencegah loop tak hingga.
Mekanisme re-scan ini bermanfaat untuk pola indirection. Dengan mendefinisikan macro perantara, kita dapat memaksa macro tertentu di-expand sebelum dioper ke operator # atau ##. Pola ini akan kita lihat pada contoh berikut.
Operator # (Stringifying) #
Operator # mengubah argumen macro menjadi string literal. Setiap argumen yang diberikan menjadi satu token string—spasi di awal/akhir dipangkas, dan tanda kutip ganda dalam argumen di-escape. Operator # hanya boleh muncul di badan macro function-like dan harus diikuti nama parameter.
#include <stdio.h>
#define STRINGIFY(x) #x
#define EXPAND_THEN_STRING(x) STRINGIFY(x)
#define VERSION 42
int main(void) {
/* Operator # mengubah token menjadi string literal */
printf("Argument: %s\n", STRINGIFY(halo dunia));
/* Versi re-ekspansi: nilai macro di-expand dulu sebelum di-stringify */
printf("VERSION expanded: %s\n", EXPAND_THEN_STRING(VERSION));
return 0;
}Output yang Dihasilkan:
Argument: halo dunia
VERSION expanded: 42Setelah preprocessing, baris di main menjadi:
printf("Argument: %s\n", "halo dunia");
printf("VERSION expanded: %s\n", "42");Perbedaan kunci: STRINGIFY(VERSION) langsung menghasilkan "VERSION" karena operator # mencegah ekspansi argumen. Sebaliknya, EXPAND_THEN_STRING(VERSION) melewati argumen VERSION ke macro perantara STRINGIFY; pada saat itu, VERSION sudah di-expand menjadi 42 oleh mekanisme re-scan, sehingga hasil akhirnya adalah "42". Pola double indirection ini adalah trik klasik untuk mengontrol order ekspansi.
Operator ## (Token Paste) #
Operator ## menggabungkan dua token menjadi satu token baru. Token hasil penggabungan harus valid sebagai identifier atau preprocessing token; jika tidak, compiler akan melaporkan error. Operator ## sering dipakai untuk menghasilkan nama variabel atau nama fungsi yang spesifik berdasarkan parameter.
#include <stdio.h>
/* Token paste operator ## menggabungkan dua token */
#define GABUNG(prefix, suffix) prefix##suffix
/* Membuat nama fungsi berbasis tipe */
#define NAMA_FUNGSI(tipe) tipe##_operasi
int int_operasi(int a, int b) { return a + b; }
double double_operasi(double a, double b) { return a * b; }
int main(void) {
/* Token paste menghasilkan identifier tunggal "mycounter" */
int GABUNG(my, counter) = 10;
printf("mycounter = %d\n", GABUNG(my, counter));
printf("int_operasi(3,4) = %d\n", NAMA_FUNGSI(int)(3, 4));
printf("double_operasi(3,4) = %.2f\n", NAMA_FUNGSI(double)(3.0, 4.0));
return 0;
}Output yang Dihasilkan:
mycounter = 10
int_operasi(3,4) = 7
double_operasi(3,4) = 12.00Mari kita telaah baris-baris kode di atas:
- Baris 4 (
#define GABUNG(prefix, suffix) prefix##suffix): macro dua parameter. Operator##menyatukanprefixdansuffixtanpa spasi. Argumenmydancounterakan menjadimycounter. - Baris 7 (
#define NAMA_FUNGSI(tipe) tipe##_operasi): macro satu parameter. Argumenintmenghasilkan tokenint_operasi, yang merupakan nama fungsi yang sudah didefinisikan di atas. - Baris 14 (
int GABUNG(my, counter) = 10;): setelah ekspansi menjadiint mycounter = 10;. Compiler melihat deklarasi variabel bernamamycounterbertipeint. - Baris 15 (
printf("mycounter = %d\n", GABUNG(my, counter));): macro kembali di-expand menjadimycounter. Karenamycountersudah dideklarasikan di baris sebelumnya, compiler menerima referensi ini. - Baris 17 (
NAMA_FUNGSI(int)(3, 4)): ekspansi menghasilkanint_operasi(3, 4). Karenaint_operasiadalah fungsi yang sudah didefinisikan, pemanggilan valid dan mengembalikan7. - Baris 18 (
NAMA_FUNGSI(double)(3.0, 4.0)): ekspansi menghasilkandouble_operasi(3.0, 4.0). Hasil perkalian3.0 * 4.0 = 12.0dicetak dengan dua angka di belakang koma.
Bahaya Side Effect Ganda #
Karena argumen macro hanya di-expand sekali per pemanggilan, argumen yang muncul berkali-kali pada badan macro menyebabkan side effect dijalankan berulang. Contoh klasik adalah MAX(i++, j++) yang sudah kita bahas di chapter Macro Parameters. Prinsip yang sama berlaku untuk semua macro berparameter multi-kemunculan argumen.
Untuk mengurangi risiko, banyak proyek modern mengganti macro berparameter dengan static inline function, yang menjamin argumen dievaluasi sekali—topik yang akan kita dalami di chapter inline Functions. Macro masih relevan untuk operasi yang benar-benar generik pada tipe data yang sangat bervariasi, atau untuk stringification dan token paste yang tidak bisa dilakukan fungsi biasa.