Makro dengan Argumen

Makro multi-baris, penempelan token ##, dan jebakan efek samping pada makro.

Daftar isi

Makro tanpa argumen yang memberi nama pada sebuah nilai sudah diperkenalkan bersama konstanta dan literal. Topik ini melangkah ke makro dengan argumen (function-like macro), yaitu makro yang menerima parameter layaknya fungsi. Pembahasannya mencakup makro multi-baris, penyambungan token dengan operator ##, serta jebakan efek samping yang menjadi ciri khas makro. Setelah membacanya, potongan kode yang dipakai berulang dapat dibungkus menjadi makro, lengkap dengan pemahaman atas batasan yang menyertainya.

Makro dengan argumen#

Makro dengan argumen didefinisikan seperti makro biasa, tetapi nama makronya langsung diikuti tanda kurung berisi daftar parameter. Ketika nama makro dipanggil di dalam kode, preprosesor menggantikan setiap parameter pada tubuh makro dengan teks argumen yang dikirim, lalu menempelkan hasilnya ke posisi pemanggilan. Penggantian terjadi pada teks sebelum kompilasi dimulai, sehingga tidak ada pemeriksaan tipe dan tidak ada biaya pemanggilan fungsi ketika program berjalan. Program berikut memakai tiga makro untuk perhitungan geometri sederhana.

c makro-dasar.c
#include <stdio.h>

#define PI 3.14159
#define KUADRAT(x) ((x) * (x))
#define KELILING_LINGKARAN(r) (2 * PI * (r))

int main(void) {
    printf("Kuadrat dari 7 adalah %d\n", KUADRAT(7));
    printf("Kuadrat dari 2 + 3 adalah %d\n", KUADRAT(2 + 3));
    printf("Keliling lingkaran jari-jari 10 adalah %.2f\n", KELILING_LINGKARAN(10));
    return 0;
}
text Keluaran
Kuadrat dari 7 adalah 49
Kuadrat dari 2 + 3 adalah 25
Keliling lingkaran jari-jari 10 adalah 62.83

Baris KUADRAT(2 + 3) memperlihatkan sifat penting makro: argumennya boleh berupa ekspresi apa pun karena yang dikirim hanyalah teks. Bentuk akhir hasil pengembangan makro dapat diperiksa dengan menghentikan gcc pada tahap preproses melalui opsi -E.

bash
# menampilkan hasil pengembangan makro tanpa melanjutkan kompilasi
gcc -E makro-dasar.c | tail -n 6
text Keluaran
int main(void) {
    printf("Kuadrat dari 7 adalah %d\n", ((7) * (7)));
    printf("Kuadrat dari 2 + 3 adalah %d\n", ((2 + 3) * (2 + 3)));
    printf("Keliling lingkaran jari-jari 10 adalah %.2f\n", (2 * 3.14159 * (10)));
    return 0;
}

Keluaran preproses memperlihatkan bahwa teks argumen sudah menggantikan setiap parameter, lengkap dengan tanda kurung pengamannya. Tanda kurung pada tubuh makro bukan hiasan. Apabila parameter ditulis tanpa pengapit, misalnya #define KUADRAT(x) x * x, pemanggilan KUADRAT(2 + 3) berkembang menjadi 2 + 3 * 2 + 3 yang bernilai 11, bukan 25. Aturan amannya adalah mengapit setiap parameter dan mengapit pula seluruh tubuh makro dengan tanda kurung.

Makro multi-baris#

Satu baris definisi cukup untuk makro yang berisi satu ekspresi. Makro yang menyusun beberapa pernyataan ditulis dengan tanda garis miring terbalik (\) di akhir setiap baris. Tanda itu menyambung baris tersebut dengan baris berikutnya dan harus menjadi karakter terakhir pada barisnya. Program berikut mendefinisikan makro TUKAR yang menukar isi dua variabel dalam tiga pernyataan.

c makro-multi-baris.c
#include <stdio.h>

#define TUKAR(a, b) do { \
    int sementara_ = (a); \
    (a) = (b); \
    (b) = sementara_; \
} while (0)

int main(void) {
    int x = 3;
    int y = 8;
    printf("Sebelum: x = %d, y = %d\n", x, y);
    TUKAR(x, y);
    printf("Sesudah: x = %d, y = %d\n", x, y);
    return 0;
}
text Keluaran
Sebelum: x = 3, y = 8
Sesudah: x = 8, y = 3

Pembungkus do { ... } while (0) pada tubuh makro terlihat berlebihan, tetapi menyelamatkan pemakaian makro di dalam percabangan. Pernyataan if (syarat) TUKAR(x, y); else tindakan(); hanya sah apabila makro berkembang menjadi satu pernyataan utuh, karena cabang if tanpa tanda kurung kurawal hanya menerima satu pernyataan. Bentuk { ... } polos akan membuat tanda titik koma setelah pemanggilan menjadi pernyataan kosong yang memutus hubungan if dengan else.

Penyambungan token dengan # dan#

Dua operator khusus bekerja pada argumen makro. Operator # yang ditempel di depan parameter mengubah teks argumennya menjadi literal string, sebuah proses yang disebut stringizing. Operator ## yang ditempel di antara dua token menyambung keduanya menjadi satu token baru, dan proses ini disebut penyambungan token (token pasting). Program berikut memakai keduanya.

c makro-token.c
#include <stdio.h>

#define TEKS(x) #x
#define GABUNG(a, b) a##b
#define BUAT_PENYIMPAN(n) int penyimpan_##n

BUAT_PENYIMPAN(uji) = 42;

int main(void) {
    printf("%s\n", TEKS(halo dunia));
    printf("%s\n", TEKS(3 + 4));
    printf("%d\n", GABUNG(12, 34));
    printf("%d\n", penyimpan_uji);
    return 0;
}
text Keluaran
halo dunia
3 + 4
1234
42

Baris BUAT_PENYIMPAN(uji) = 42; memperlihatkan kegunaan utama ##, yaitu menghasilkan deklarasi baru saat kompilasi. Makro itu berkembang menjadi deklarasi int penyimpan_uji = 42; yang tidak pernah ditulis secara utuh di kode sumber. Pola semacam ini disebut kode generatif dan dipakai untuk mendaftarkan nama yang berulang, misalnya nama enum, nama fungsi uji, atau nama variabel per modul. Satu baris makro cukup untuk setiap nama yang didaftarkan.

Penyambungan hanya sah apabila hasilnya tetap merupakan token yang bermakna. Penyambungan 12 dan 34 menghasilkan token angka 1234, sedangkan penyambungan penyimpan_ dan uji menghasilkan pengenal baru. Apabila hasilnya bukan token yang sah, misalnya menyambung dua operator yang tidak berpasangan, kompiler menolaknya ketika token hasil itu dipakai. Batasan ini menempatkan ## sebagai alat penamaan, bukan alat perhitungan.

Jebakan makro: efek samping#

Makro menyalin teks, bukan menilai nilai. Karena itu, argumen yang memuat efek samping dievaluasi ulang pada setiap kemunculan parameternya di tubuh makro. Efek samping (side effect) adalah perubahan keadaan yang menyertai sebuah ekspresi, misalnya penambahan pada i++ atau penugasan. Program berikut membandingkan pemanggilan MAKS dengan argumen polos dan dengan argumen yang menaikkan nilai.

c makro-efek-samping.c
#include <stdio.h>

#define MAKS(a, b) ((a) > (b) ? (a) : (b))

int main(void) {
    int i = 5;
    int j = 2;
    printf("MAKS(%d, %d) = %d\n", i, j, MAKS(i, j));
    printf("Nilai i tetap %d\n", i);

    int k = 5;
    int m = 2;
    printf("MAKS(k++, m) menghasilkan %d\n", MAKS(k++, m));
    printf("Nilai k menjadi %d\n", k);
    return 0;
}
text Keluaran
MAKS(5, 2) = 5
Nilai i tetap 5
MAKS(k++, m) menghasilkan 6
Nilai k menjadi 7

Pemanggilan MAKS(k++, m) berkembang menjadi ((k++) > (m) ? (k++) : (m)). Parameter a muncul dua kali pada tubuh makro, sehingga k++ dijalankan dua kali, yaitu sekali pada kondisi dan sekali lagi pada cabang yang terpilih. Akibatnya, hasil pemanggilannya 6 dan k melompat ke 7, padahal fungsi biasa bernama maks hanya akan menaikkan k satu kali dan mengembalikan 5. Perbaikannya bukan pada tubuh makro, melainkan pada pemanggilannya. Simpan nilai yang memiliki efek samping ke variabel terpisah sebelum dikirim ke makro, atau gunakan fungsi ketika pemeriksaan tipe diinginkan.

Kesalahan umum#

Empat kesalahan berikut paling sering muncul ketika makro berargumen ditulis dan dipanggil.

  • Memberi spasi antara nama makro dan tanda kurung parameternya. Definisi #define KUADRAT (x) ((x) * (x)) tidak lagi membuat makro berargumen, melainkan makro objek bernama KUADRAT yang berkembang menjadi (x) ((x) * (x)). Pemanggilan KUADRAT(7) kemudian berakhir sebagai error sintaks. Tanda kurung parameter harus menempel pada nama makro.
  • Menulis parameter tanpa tanda kurung. Definisi #define JUMLAH(a, b) a + b membuat pemanggilan JUMLAH(2, 3) * 4 berkembang menjadi 2 + 3 * 4 yang bernilai 14, bukan 20. Apit setiap parameter dengan tanda kurung, lalu apit pula seluruh tubuh makronya.
  • Menaruh titik koma di akhir definisi makro. Definisi #define CETAK(x) printf("%d\n", x); menambahkan titik koma pada setiap pemanggilan, sehingga if (syarat) CETAK(x); else tindakan(); gagal dikompilasi karena titik koma bekas makro memutus hubungan if dengan else. Titik koma milik pemanggil, bukan milik definisi makro.
  • Menyisipkan efek samping pada argumen tanpa menyadarinya. Makro yang parameternya muncul lebih dari satu kali mengeksekusi i++ atau n-- sebanyak kemunculan itu, dan hasilnya tidak selalu terasa selagi kode masih sederhana. Simpan nilai yang menaikkan atau menurunkan ke variabel terpisah sebelum masuk ke makro.

Ringkasan#

  • Makro berargumen menggantikan parameter pada tubuhnya dengan teks argumen sebelum kompilasi, tanpa pemeriksaan tipe dan tanpa pemanggilan ketika program berjalan.
  • Tanda garis miring terbalik menyambung definisi multi-baris, dan pembungkus do { ... } while (0) menjadikan makro multi-pernyataan aman di semua posisi pernyataan.
  • Operator # mengubah argumen menjadi literal string, sedangkan operator ## menyambung dua token menjadi satu untuk menghasilkan nama baru saat kompilasi.
  • Argumen yang memuat efek samping dievaluasi sebanyak kemunculan parameternya, sehingga nilai seperti i++ perlu disimpan ke variabel terpisah terlebih dahulu.

Referensi#