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.

c
#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:

text
Argument: halo dunia
VERSION expanded: 42

Setelah preprocessing, baris di main menjadi:

text
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.

c
#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:

text
mycounter = 10
int_operasi(3,4)    = 7
double_operasi(3,4) = 12.00

Mari kita telaah baris-baris kode di atas:

  1. Baris 4 (#define GABUNG(prefix, suffix) prefix##suffix): macro dua parameter. Operator ## menyatukan prefix dan suffix tanpa spasi. Argumen my dan counter akan menjadi mycounter.
  2. Baris 7 (#define NAMA_FUNGSI(tipe) tipe##_operasi): macro satu parameter. Argumen int menghasilkan token int_operasi, yang merupakan nama fungsi yang sudah didefinisikan di atas.
  3. Baris 14 (int GABUNG(my, counter) = 10;): setelah ekspansi menjadi int mycounter = 10;. Compiler melihat deklarasi variabel bernama mycounter bertipe int.
  4. Baris 15 (printf("mycounter = %d\n", GABUNG(my, counter));): macro kembali di-expand menjadi mycounter. Karena mycounter sudah dideklarasikan di baris sebelumnya, compiler menerima referensi ini.
  5. Baris 17 (NAMA_FUNGSI(int)(3, 4)): ekspansi menghasilkan int_operasi(3, 4). Karena int_operasi adalah fungsi yang sudah didefinisikan, pemanggilan valid dan mengembalikan 7.
  6. Baris 18 (NAMA_FUNGSI(double)(3.0, 4.0)): ekspansi menghasilkan double_operasi(3.0, 4.0). Hasil perkalian 3.0 * 4.0 = 12.0 dicetak 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.