Bahasa C ++ menawarkan kemungkinan luas untuk melakukannya tanpa makro. Jadi mari kita coba gunakan makro sesedikit mungkin!
Segera buat reservasi bahwa saya bukan seorang fanatik dan tidak mendesak untuk meninggalkan makro karena alasan idealis. Sebagai contoh, ketika datang untuk secara manual menghasilkan jenis kode yang sama, saya dapat mengenali manfaat makro dan menerima persyaratannya. Sebagai contoh, saya dengan tenang berhubungan dengan makro dalam program lama yang ditulis menggunakan MFC. Tidak masuk akal untuk bertarung dengan sesuatu seperti ini:
BEGIN_MESSAGE_MAP(efcDialog, EFCDIALOG_PARENT )
Ada makro seperti itu, dan oke. Mereka benar-benar diciptakan untuk menyederhanakan pemrograman.
Saya berbicara tentang makro lain yang dengannya mereka mencoba menghindari implementasi fungsi lengkap atau mencoba mengurangi ukuran fungsi. Pertimbangkan beberapa motif untuk menghindari makro seperti itu.
Catatan Teks ini ditulis sebagai posting tamu untuk blog Simplify C ++. Saya memutuskan untuk menerbitkan artikel versi Rusia di sini. Sebenarnya, saya menulis catatan ini untuk menghindari pertanyaan dari pembaca yang lalai mengapa artikel ini tidak ditandai sebagai "terjemahan" :). Dan di sini, sebenarnya, posting tamu dalam bahasa Inggris: " Makro Jahat dalam Kode C ++ ".Pertama: kode makro menarik bug
Saya tidak tahu bagaimana menjelaskan alasan fenomena ini dari sudut pandang filosofis, tetapi memang demikian. Selain itu, bug yang berhubungan dengan makro seringkali sangat sulit dikenali saat melakukan peninjauan kode.
Saya telah berulang kali menggambarkan kasus-kasus seperti itu di artikel saya. Misalnya,
mengganti fungsi
isspace dengan makro ini:
#define isspace(c) ((c)==' ' || (c) == '\t')
Programmer yang menggunakan
isspace percaya bahwa ia menggunakan fungsi nyata yang menganggap tidak hanya spasi dan tab sebagai spasi, tetapi juga LF, CR, dll. Hasilnya adalah bahwa salah satu syarat selalu benar dan kode tidak berfungsi sebagaimana dimaksud. Kesalahan dari Komandan Tengah Malam ini dijelaskan di
sini .
Atau bagaimana Anda menyukai singkatan ini untuk menulis fungsi
std :: printf ?
#define sprintf std::printf
Saya pikir pembaca menebak bahwa itu adalah makro yang sangat tidak berhasil. Omong-omong, itu ditemukan di proyek StarEngine. Baca lebih lanjut tentang ini di
sini .
Orang bisa berargumen bahwa programmer harus disalahkan atas kesalahan ini, bukan makro. Begitulah. Tentu, programmer selalu disalahkan atas kesalahan :).
Penting bahwa makro menyebabkan kesalahan. Ternyata macro harus digunakan dengan akurasi yang meningkat atau tidak sama sekali.
Saya bisa memberikan contoh cacat yang terkait dengan penggunaan makro untuk waktu yang lama, dan catatan yang bagus ini akan berubah menjadi dokumen multi-halaman yang berat. Tentu saja, saya tidak akan melakukan ini, tetapi saya akan menunjukkan beberapa kasus lain untuk persuasif.
Pustaka ATL
menyediakan makro seperti A2W, T2W, dan sebagainya untuk mengonversi string. Namun, beberapa orang tahu bahwa makro ini sangat berbahaya untuk digunakan di dalam loop. Di dalam makro, fungsi
alokasi disebut, yang akan mengalokasikan memori pada tumpukan lagi dan lagi pada setiap iterasi loop. Suatu program dapat berpura-pura bekerja dengan benar. Segera setelah program mulai memproses garis panjang atau jumlah iterasi dalam loop meningkat, tumpukan dapat diambil dan diakhiri pada saat yang paling tidak terduga. Anda dapat membaca lebih lanjut tentang ini di
mini-book ini (lihat bab βJangan panggil fungsi alokasi () di dalam loopβ).
Makro seperti A2W menyembunyikan kejahatan. Mereka terlihat seperti fungsi, tetapi, pada kenyataannya, memiliki efek samping yang sulit untuk diperhatikan.
Saya tidak bisa melewati upaya serupa untuk mengurangi kode menggunakan makro:
void initialize_sanitizer_builtins (void) { .... #define DEF_SANITIZER_BUILTIN(ENUM, NAME, TYPE, ATTRS) \ decl = add_builtin_function ("__builtin_" NAME, TYPE, ENUM, \ BUILT_IN_NORMAL, NAME, NULL_TREE); \ set_call_expr_flags (decl, ATTRS); \ set_builtin_decl (ENUM, decl, true); #include "sanitizer.def" if ((flag_sanitize & SANITIZE_OBJECT_SIZE) && !builtin_decl_implicit_p (BUILT_IN_OBJECT_SIZE)) DEF_SANITIZER_BUILTIN (BUILT_IN_OBJECT_SIZE, "object_size", BT_FN_SIZE_CONST_PTR_INT, ATTR_PURE_NOTHROW_LEAF_LIST) .... }
Hanya baris pertama makro yang merujuk ke
pernyataan if . Baris yang tersisa akan dieksekusi terlepas dari kondisinya. Kita dapat mengatakan bahwa kesalahan ini berasal dari dunia C, karena ditemukan oleh saya menggunakan diagnostik
V640 di dalam kompiler GCC. Kode GCC ditulis terutama dalam C, dan dalam bahasa ini makro sulit dilakukan. Namun, Anda harus mengakui bahwa ini bukan masalahnya. Di sini sangat memungkinkan untuk membuat fungsi nyata.
Kedua: membaca kode menjadi lebih rumit
Jika Anda menemukan proyek yang penuh dengan makro yang terdiri dari makro lain, maka Anda mengerti apa itu untuk memahami proyek seperti itu. Jika Anda belum menemukan, maka ambil satu kata, ini menyedihkan. Sebagai contoh kode yang sulit dibaca, saya dapat mengutip kompiler GCC yang disebutkan sebelumnya.
Menurut legenda, Apple telah berinvestasi dalam pengembangan proyek LLVM sebagai alternatif untuk GCC karena kompleksitas kode GCC karena makro ini. Di mana saya membacanya, saya tidak ingat, jadi tidak akan ada bukti.
Ketiga: menulis makro itu sulit
Sangat mudah untuk menulis makro yang buruk. Saya bertemu mereka di mana-mana dengan konsekuensi yang sesuai. Tetapi menulis makro yang baik dan dapat diandalkan seringkali lebih sulit daripada menulis fungsi yang serupa.
Menulis makro yang baik sulit karena alasan itu, tidak seperti fungsi, itu tidak dapat dianggap sebagai entitas independen. Diperlukan untuk segera mempertimbangkan makro dalam konteks semua opsi yang memungkinkan untuk penggunaannya, jika tidak, sangat mudah untuk menyapu masalah bentuk:
#define MIN(X, Y) (((X) < (Y)) ? (X) : (Y)) m = MIN(ArrayA[i++], ArrayB[j++]);
Tentu saja, untuk kasus seperti itu, solusi telah lama ditemukan dan makro dapat diimplementasikan dengan aman:
#define MAX(a,b) \ ({ __typeof__ (a) _a = (a); \ __typeof__ (b) _b = (b); \ _a > _b ? _a : _b; })
Satu-satunya pertanyaan adalah, apakah kita membutuhkan semua ini di C ++? Tidak, di C ++ ada template dan cara lain untuk membangun kode yang efisien. Jadi mengapa saya terus menemukan makro serupa di program C ++?
Keempat: debugging itu rumit
Ada pendapat bahwa debugging adalah untuk para pengecut :). Ini, tentu saja, menarik untuk dibahas, tetapi dari sudut pandang praktis, debugging berguna dan membantu untuk menemukan kesalahan. Makro mempersulit proses ini dan tentu saja memperlambat pencarian kesalahan.
Kelima: positif palsu dari penganalisa statis
Banyak makro, karena spesifik perangkat mereka, menghasilkan beberapa positif palsu dari penganalisa kode statis. Saya dapat dengan aman mengatakan bahwa sebagian besar positif palsu ketika memeriksa kode C dan C ++ terkait dengan makro.
Masalahnya dengan makro adalah bahwa alat analisis tidak dapat membedakan kode rumit yang benar dari kode yang salah.
Artikel tentang memeriksa Chromium menjelaskan salah satu makro ini.
Apa yang harus dilakukan
Jangan gunakan makro dalam program C ++ kecuali benar-benar diperlukan!
C ++ menyediakan alat yang kaya seperti fungsi templat, inferensi tipe otomatis (otomatis, jenis deklarasi), fungsi constexpr.
Hampir selalu, alih-alih makro, Anda dapat menulis fungsi biasa. Seringkali ini tidak dilakukan karena kemalasan biasa. Kemalasan ini berbahaya, dan kita harus memeranginya. Waktu tambahan kecil yang dihabiskan untuk menulis fungsi penuh akan terbayar dengan bunga. Kode akan lebih mudah dibaca dan dipelihara. Kemungkinan menembak kaki Anda sendiri akan berkurang, dan kompiler dan analisis statis akan menghasilkan lebih sedikit hasil positif palsu.
Beberapa mungkin berpendapat bahwa kode dengan fungsi kurang efisien. Ini juga hanya "alasan".
Kompiler sekarang dengan sempurna memasukkan kode, bahkan jika Anda belum menulis
kata kunci sebaris .
Jika kita berbicara tentang menghitung ekspresi pada tahap kompilasi, maka di sini makro tidak diperlukan dan bahkan berbahaya. Untuk tujuan yang sama, jauh lebih baik dan lebih aman untuk menggunakan
constexpr .
Saya akan jelaskan dengan sebuah contoh. Ini adalah kesalahan makro klasik yang saya
pinjam dari kode Kernel FreeBSD.
#define ICB2400_VPOPT_WRITE_SIZE 20 #define ICB2400_VPINFO_PORT_OFF(chan) \ (ICB2400_VPINFO_OFF + \ sizeof (isp_icb_2400_vpinfo_t) + \ (chan * ICB2400_VPOPT_WRITE_SIZE))
Argumen
chan digunakan dalam makro tanpa membungkus tanda kurung. Akibatnya, ekspresi
ICB2400_VPOPT_WRITE_SIZE tidak menggandakan ekspresi
(chan - 1) , tetapi hanya satu.
Kesalahan tidak akan muncul jika fungsi biasa ditulis alih-alih makro.
size_t ICB2400_VPINFO_PORT_OFF(size_t chan) { return ICB2400_VPINFO_OFF + sizeof(isp_icb_2400_vpinfo_t) + chan * ICB2400_VPOPT_WRITE_SIZE; }
Sangat mungkin bahwa kompiler C dan C ++ modern akan secara independen melakukan
inlining dari fungsi, dan kode akan seefisien dalam kasus makro.
Pada saat yang sama, kode tersebut menjadi lebih mudah dibaca, serta bebas dari kesalahan.
Jika diketahui bahwa nilai input selalu konstan, maka Anda dapat menambahkan
constexpr dan memastikan bahwa semua perhitungan akan terjadi pada tahap kompilasi. Bayangkan itu adalah C ++ dan
chan itu selalu konstan. Maka berguna untuk mendeklarasikan fungsi
ICB2400_VPINFO_PORT_OFF seperti ini:
constexpr size_t ICB2400_VPINFO_PORT_OFF(size_t chan) { return ICB2400_VPINFO_OFF + sizeof(isp_icb_2400_vpinfo_t) + chan * ICB2400_VPOPT_WRITE_SIZE; }
Untung!
Saya harap saya berhasil meyakinkan Anda. Semoga berhasil dan lebih sedikit makro dalam kode!