Menu

Makro di C: #define, makro seperti fungsi, dan aturan tanda kurung

Makro seperti objek dan seperti fungsi dengan #define: kenapa setiap argumen dan seluruh badan makro butuh tanda kurung, jebakan evaluasi berulang, makro multi-baris dengan do-while(0), dan kapan fungsi atau const lebih tepat.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Makro adalah nama yang diganti preprocessor dengan teks tertentu sebelum compiler berjalan. Hanya itu mekanismenya, dan setiap aturan di halaman ini adalah akibat dari hal itu: makro tidak punya tipe, tidak menghormati scope, tidak mengevaluasi argumen, dan tidak tahu apa itu pemanggilan fungsi. Makro hanya menyalin teks.

Kalau dipakai dengan hati-hati, makro sangat dibutuhkan: saklar portabilitas, makro assertion yang menangkap nomor baris, konstanta waktu kompilasi yang bisa dipakai untuk ukuran array. Kalau dipakai sembarangan, makro menghasilkan bug yang kelihatan mustahil sampai kamu mengekspansi file-nya.

Makro seperti objek

Bentuk paling sederhana mendefinisikan nama yang mewakili sebuah nilai:

Biasanya nama-nama ini ditulis UPPER_SNAKE_CASE, supaya pembaca langsung tahu bahwa sebuah simbol adalah makro, bukan variabel. Konvensi ini lebih penting dari biasanya, karena makro mengabaikan scope: #define di dalam sebuah fungsi tetap berlaku untuk sisa file, dan dengan senang hati akan menulis ulang variabel bernama sama di fungsi lain.

#undef NAME menghapus sebuah definisi, sehingga nama itu bisa didefinisikan ulang nanti tanpa peringatan soal redefinisi.

Makro seperti fungsi

Tulis daftar parameter tepat setelah nama, tanpa spasi sebelum tanda kurung buka, maka makro itu menerima argumen:

Aturan spasi ini nyata: #define SQUARE (x) ((x)*(x)) mendefinisikan makro seperti objek bernama SQUARE yang teks penggantinya diawali (x). Error compiler yang muncul sesudahnya tidak akan menyebut soal spasi.

Aturan tanda kurung

Ada dua aturan, dan keduanya ada karena badan makro ditempel ke dalam ekspresi yang tidak bisa kamu lihat saat menulis makronya.

Aturan 1: beri tanda kurung pada setiap parameter. Tanpa itu, argumen yang berupa ekspresi akan tercerai-berai oleh prioritas operator:

#define SQUARE_BAD(x) x * x

SQUARE_BAD(2 + 3)   /* becomes 2 + 3 * 2 + 3  ==  11, not 25 */

Aturan 2: beri tanda kurung pada seluruh badan. Tanpa itu, ekspresi di sekitarnya yang mencerai-beraikan hasilnya:

#define DOUBLE_BAD(x) (x) + (x)

10 / DOUBLE_BAD(5)  /* becomes 10 / (5) + (5)  ==  7, not 1 */

Jalankan kedua kegagalan itu berdampingan dengan versi yang sudah diperbaiki:

Kedua versi yang salah bisa dikompilasi tanpa satu peringatan pun dan menghasilkan angka yang salah. Kebiasaan yang perlu dibangun bersifat mekanis: bungkus setiap parameter, lalu bungkus hasilnya. ((a) > (b) ? (a) : (b)) memang kelihatan berisik, dan justru itu tujuannya.

Jebakan evaluasi berulang

Tanda kurung tidak bisa menyelamatkanmu dari bahaya kedua. Makro menempelkan teks argumennya di setiap tempat parameter itu muncul, jadi argumen yang punya efek samping akan terjadi lebih dari sekali:

Makro itu diekspansi menjadi ((i++) > (j) ? (i++) : (j)). Perbandingannya menaikkan i menjadi 6 dan membandingkan 5 dengan 3; cabang benarnya menaikkan i lagi menjadi 7 dan menghasilkan 6. Jadi m bernilai 6 dan i bernilai 7, dan keduanya bukan yang seharusnya diberikan "nilai terbesar dari i dan j". Fungsi max(i++, j) akan benar, karena argumennya dievaluasi sekali sebelum pemanggilan.

Jebakan yang sama menangkap apa pun yang punya efek samping: SQUARE(rand()) memanggil rand() dua kali dan mengalikan dua angka yang berbeda. CHECK(read_byte()) menghabiskan dua byte.

Dua cara bertahan, dari yang paling disarankan:

  1. Pakai fungsi sungguhan. Kalau kamu membutuhkannya untuk beberapa tipe, tulis satu per tipe, atau pakai fungsi static inline di header.
  2. Kalau memang harus makro, tulis dengan jelas di dokumentasinya bahwa argumen dievaluasi lebih dari sekali, dan jaga agar tempat pemanggilannya bebas dari efek samping.

Makro multi-baris dan do-while(0)

Badan makro bisa memanjang ke beberapa baris kalau setiap baris kecuali yang terakhir diakhiri backslash. Versi naifnya kelihatan baik-baik saja:

#define LOG_TWICE(msg) \
    printf("%s\n", msg); \
    printf("%s\n", msg)

Lalu makro itu rusak, diam-diam, tepat di tempat yang penting:

if (verbose)
    LOG_TWICE("hello");
else
    printf("quiet\n");

Setelah substitusi, if hanya memiliki printf pertama, yang kedua berjalan tanpa syarat, dan else sekarang tidak punya pasangan if: error kompilasi yang pesannya tidak menunjuk ke tempat yang berguna. Membungkus badannya dengan kurung kurawal biasa juga tidak lebih baik: ; di belakang LOG_TWICE("hello") menjadi pernyataan kosong yang mengakhiri if, dan else rusak lagi.

Idiom yang berhasil adalah do { ... } while (0):

do { ... } while (0) adalah satu pernyataan, menjalankan badannya tepat satu kali, dan mengharuskan titik koma sesudahnya. Jadi tempat pemanggilannya terbaca seperti pemanggilan fungsi biasa dan berperilaku seperti itu di setiap konteks alur kontrol. Hati-hati dengan backslash: spasi yang nyasar setelah backslash mengakhiri sambungan baris dan menghasilkan error yang membingungkan.

Makro bawaan dan pola assertion

Preprocessor mendefinisikan beberapa makro sendiri, dan makro-makro inilah alasan beberapa hal memang tidak bisa dijadikan fungsi:

__FILE__ dan __LINE__ diekspansi di tempat keduanya ditulis, jadi menaruhnya di dalam makro akan menangkap posisi pemanggil. Fungsi tidak bisa melakukan ini: di dalam fungsi, keduanya akan selalu melaporkan file dan baris milik fungsi logging itu sendiri. Persis begitulah makro standar assert melaporkan lokasi ekspresi yang gagal.

Nama bawaan lain yang berguna: __DATE__, __TIME__, dan __STDC_VERSION__ (misalnya 201710L untuk C17).

Dua operator yang perlu diketahui

Di dalam badan makro, # mengubah parameter menjadi string literal ("stringizing") dan ## menempelkan token menjadi satu ("token pasting").

SHOW(width * height) menjadi printf("width * height" " = %d\n", (width * height));. String literal yang bersebelahan digabungkan oleh compiler, jadi satu pemanggilan mencetak teks ekspresinya sekaligus nilainya. Ini trik debugging yang layak diingat.

## lebih jarang dan kebanyakan muncul di makro pembangkit kode: #define MAKE_VAR(n) int var_##n mengubah MAKE_VAR(3) menjadi int var_3. Pakai secukupnya; identifier yang dibangun oleh preprocessor tidak bisa dicari berdasarkan namanya, sehingga kodenya sulit dijelajahi.

Makro vs fungsi vs const

Pakai alat paling lemah yang sudah cukup untuk pekerjaannya:

  • Variabel const: const double PI = 3.14159; punya tipe, mengikuti scope, terlihat di debugger, dan tidak bisa didefinisikan ulang oleh header yang tidak berhubungan. Utamakan ini untuk nilai yang dipakai saat runtime.
  • enum: enum { MAX_USERS = 100 }; memberi konstanta integer bernama yang bisa dipakai di tempat C menuntut konstanta waktu kompilasi, lengkap dengan tipe dan scope. Cocok untuk batas berupa integer.
  • Fungsi static inline: tipenya diperiksa, setiap argumen dievaluasi tepat satu kali, dan compiler modern meng-inline-nya sama seperti makro. Inilah pengganti yang tepat untuk hampir semua makro seperti fungsi.
  • Makro: saat kamu butuh __LINE__, saat sesuatu harus bekerja sebelum tipe ada (ukuran array di kode lama), saat kamu mengganti-ganti kode dengan kompilasi bersyarat, atau saat kamu menghasilkan kode yang berulang.

Satu tempat yang masih dikuasai makro adalah menghitung ukuran array, karena hasilnya harus berupa konstanta waktu kompilasi:

Makro ini punya catatan terkenalnya sendiri: hanya benar untuk array sungguhan. Kirim pointer, yang memang jadi bentuk array saat diberikan ke fungsi, maka sizeof mengukur pointernya dan memberi jawaban yang salah tanpa tanda apa pun.

Debug makro

Saat sebuah makro berperilaku aneh, jangan cuma dipelototi. Ekspansikan:

gcc -E program.c | tail -30

Teks hasil substitusi langsung memberitahumu apakah masalahnya tanda kurung yang hilang, evaluasi ganda, atau nama yang bentrok dengan hal lain. Kompilasi juga dengan -Wall -Wextra: GCC dan clang menambahkan catatan "in expansion of macro" yang menghubungkan baris error yang dilaporkan ke definisinya.

Selanjutnya: file header, tempat #define dan #include bekerja sama sehingga satu proyek bisa tersebar di banyak file source tanpa menempelkan deklarasi yang sama dua kali.

Pertanyaan yang sering diajukan

Apa itu makro di C?

Makro adalah potongan teks bernama yang disisipkan preprocessor ke kode sumbermu sebelum kompilasi. #define MAX 100 membuat setiap MAX sesudahnya menjadi 100; #define SQUARE(x) ((x) * (x)) menerima argumen dan menyisipkannya ke dalam sebuah pola. Makro tidak punya tipe dan tidak mengikuti aturan scope: makro hanyalah penggantian teks.

Kenapa makro C butuh begitu banyak tanda kurung?

Karena badannya ditempel ke kode di sekitarnya lalu di-parse sebagai satu kesatuan. #define SQUARE(x) x * x mengubah SQUARE(2 + 3) menjadi 2 + 3 * 2 + 3, yang hasilnya 11, bukan 25. Membungkus setiap argumen dan seluruh badan, ((x) * (x)), membuat teks hasil substitusi terkelompok sesuai maksudmu, apa pun yang ada di sekitarnya.

Apa bedanya makro dan fungsi di C?

Fungsi dikompilasi sekali, tipenya diperiksa, dan setiap argumen dievaluasi tepat satu kali. Makro ditempel di setiap pemakaian, tidak memeriksa apa pun, dan bisa mengevaluasi argumen berkali-kali, sehingga MAX(i++, j) bisa menaikkan i dua kali. Utamakan fungsi (serta const/enum untuk konstanta); pakai makro untuk hal yang tidak bisa dilakukan fungsi, seperti menangkap __LINE__ atau menghasilkan kode.

Kenapa makro multi-baris dibungkus do { ... } while (0)?

Supaya makro itu berperilaku seperti satu pernyataan. Badan { ... } biasa akan rusak kalau diikuti titik koma di dalam if/else (titik komanya mengakhiri if terlalu awal), dan deretan pernyataan biasa akan rusak di if tanpa kurung kurawal. do { ... } while (0) adalah satu pernyataan yang menerima titik koma di belakangnya dan menjalankan badannya tepat satu kali.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai