Saya selalu menanyakan hal ini tetapi saya tidak pernah menerima jawaban yang benar-benar bagus; Saya pikir hampir semua programmer bahkan sebelum menulis "Hello World" yang pertama telah menemukan frase seperti "makro tidak boleh digunakan", "makro jahat" dan seterusnya, pertanyaan saya adalah: mengapa? Dengan C ++ 11 baru, apakah ada alternatif nyata setelah bertahun-tahun?
Bagian yang mudah adalah tentang makro seperti #pragma
, yang spesifik untuk platform dan spesifik kompilator, dan sebagian besar waktu mereka memiliki kelemahan serius seperti #pragma once
itu adalah rawan kesalahan setidaknya dalam 2 situasi penting: nama yang sama di jalur yang berbeda dan dengan beberapa pengaturan jaringan dan sistem file.
Namun secara umum, bagaimana dengan makro dan alternatif penggunaannya?
sumber
#pragma
bukan makro.#pragma
.constexpr
,inline
fungsi, dantemplates
, tetapiboost.preprocessor
danchaos
menunjukkan bahwa makro memiliki tempatnya. Belum lagi makro konfigurasi untuk kompiler perbedaan, platform, dll.Jawaban:
Makro sama seperti alat lainnya - palu yang digunakan dalam pembunuhan tidak jahat karena ini adalah palu. Cara orang menggunakannya dengan cara itu adalah kejahatan. Jika Anda ingin palu dengan palu, palu adalah alat yang sempurna.
Ada beberapa aspek pada makro yang membuatnya "buruk" (saya akan mengembangkannya nanti, dan menyarankan alternatif):
Jadi mari kita kembangkan sedikit di sini:
1) Makro tidak dapat di-debug. Ketika Anda memiliki makro yang diterjemahkan menjadi angka atau string, kode sumber akan memiliki nama makro, dan banyak debugger, Anda tidak dapat "melihat" untuk apa makro diterjemahkan. Jadi, Anda sebenarnya tidak tahu apa yang sedang terjadi.
Penggantian : Gunakan
enum
atauconst T
Untuk makro "seperti fungsi", karena debugger bekerja pada tingkat "per baris sumber di mana Anda berada", makro Anda akan bertindak seperti pernyataan tunggal, tidak peduli apakah itu satu atau seratus pernyataan. Membuat sulit untuk mengetahui apa yang sedang terjadi.
Penggantian : Gunakan fungsi - sebaris jika perlu "cepat" (tetapi berhati-hatilah karena terlalu banyak sebaris bukanlah hal yang baik)
2) Ekspansi makro dapat memiliki efek samping yang aneh.
Yang terkenal adalah
#define SQUARE(x) ((x) * (x))
dan kegunaannyax2 = SQUARE(x++)
. Itu mengarah kex2 = (x++) * (x++);
, yang, bahkan jika itu adalah kode yang valid [1], hampir pasti tidak akan menjadi apa yang diinginkan oleh programmer. Jika itu adalah sebuah fungsi, tidak masalah untuk melakukan x ++, dan x hanya akan bertambah sekali.Contoh lainnya adalah "if else" di makro, katakanlah kita memiliki ini:
#define safe_divide(res, x, y) if (y != 0) res = x/y;
lalu
if (something) safe_divide(b, a, x); else printf("Something is not set...");
Ini benar-benar menjadi hal yang salah ....
Penggantian : fungsi nyata.
3) Makro tidak memiliki namespace
Jika kami memiliki makro:
#define begin() x = 0
dan kami memiliki beberapa kode dalam C ++ yang menggunakan begin:
std::vector<int> v; ... stuff is loaded into v ... for (std::vector<int>::iterator it = myvector.begin() ; it != myvector.end(); ++it) std::cout << ' ' << *it;
Sekarang, pesan kesalahan apa yang menurut Anda Anda dapatkan, dan di mana Anda mencari kesalahan [dengan asumsi Anda benar-benar lupa - atau bahkan tidak tahu tentang - makro awal yang ada di beberapa file header yang ditulis orang lain? [dan bahkan lebih menyenangkan jika Anda memasukkan makro itu sebelum penyertaan - Anda akan tenggelam dalam kesalahan aneh yang sama sekali tidak masuk akal saat Anda melihat kode itu sendiri.
Penggantian : Tidak ada banyak pengganti sebagai "aturan" - hanya gunakan nama huruf besar untuk makro, dan jangan pernah menggunakan semua nama huruf besar untuk hal lain.
4) Makro memiliki efek yang tidak Anda sadari
Ambil fungsi ini:
#define begin() x = 0 #define end() x = 17 ... a few thousand lines of stuff here ... void dostuff() { int x = 7; begin(); ... more code using x ... printf("x=%d\n", x); end(); }
Sekarang, tanpa melihat makro, Anda akan berpikir bahwa begin adalah fungsi, yang seharusnya tidak mempengaruhi x.
Hal semacam ini, dan saya telah melihat contoh yang jauh lebih kompleks, BENAR-BENAR dapat mengacaukan hari Anda!
Penggantian : Jangan gunakan makro untuk menyetel x, atau berikan x sebagai argumen.
Ada kalanya menggunakan makro pasti menguntungkan. Salah satu contohnya adalah membungkus fungsi dengan makro untuk meneruskan informasi file / baris:
#define malloc(x) my_debug_malloc(x, __FILE__, __LINE__) #define free(x) my_debug_free(x, __FILE__, __LINE__)
Sekarang kita dapat menggunakan
my_debug_malloc
malloc biasa dalam kode, tetapi memiliki argumen tambahan, jadi ketika sampai pada akhir dan kita memindai "elemen memori mana yang belum dibebaskan", kita dapat mencetak di mana alokasi dibuat sehingga programmer dapat melacak kebocoran.[1] Ini adalah perilaku tidak terdefinisi untuk memperbarui satu variabel lebih dari sekali "dalam titik urutan". Titik urutan tidak persis sama dengan pernyataan, tetapi untuk sebagian besar maksud dan tujuan, itulah yang harus kita anggap sebagai. Jadi melakukan
x++ * x++
akan memperbaruix
dua kali, yang tidak ditentukan dan mungkin akan mengarah pada nilai yang berbeda pada sistem yang berbeda, dan nilai hasil yang berbedax
juga.sumber
if else
bisa diatasi dengan membungkus badan makro di dalamnyado { ... } while(0)
. Ini berperilaku seperti yang diharapkan sehubungan denganif
danfor
dan masalah aliran kontrol yang berpotensi berisiko lainnya. Tapi ya, fungsi sebenarnya biasanya merupakan solusi yang lebih baik.#define macro(arg1) do { int x = func(arg1); func2(x0); } while(0)
note: expanded from macro 'begin'
dan menunjukkan di manabegin
didefinisikan.Pepatah "makro itu jahat" biasanya mengacu pada penggunaan #define, bukan #pragma.
Secara khusus, ekspresi tersebut mengacu pada dua kasus berikut:
mendefinisikan angka ajaib sebagai makro
menggunakan makro untuk mengganti ekspresi
Ya, untuk item dalam daftar di atas (angka ajaib harus didefinisikan dengan const / constexpr dan ekspresi harus ditentukan dengan fungsi [normal / inline / template / inline template].
Berikut adalah beberapa masalah yang muncul dengan mendefinisikan bilangan ajaib sebagai makro dan mengganti ekspresi dengan makro (alih-alih mendefinisikan fungsi untuk mengevaluasi ekspresi tersebut):
ketika mendefinisikan makro untuk angka ajaib, compiler tidak menyimpan informasi tipe untuk nilai yang ditentukan. Hal ini dapat menyebabkan peringatan kompilasi (dan kesalahan) dan membingungkan orang yang men-debug kode.
ketika mendefinisikan makro alih-alih fungsi, pemrogram yang menggunakan kode itu mengharapkan mereka untuk bekerja seperti fungsi dan sebenarnya tidak.
Pertimbangkan kode ini:
#define max(a, b) ( ((a) > (b)) ? (a) : (b) ) int a = 5; int b = 4; int c = max(++a, b);
Anda akan mengharapkan a dan c menjadi 6 setelah penugasan ke c (seperti yang akan terjadi, dengan menggunakan std :: max sebagai ganti makro). Sebagai gantinya, kode tersebut melakukan:
int c = ( ((++a) ? (b)) ? (++a) : (b) ); // after this, c = a = 7
Selain itu, makro tidak mendukung ruang nama, yang berarti bahwa menentukan makro dalam kode Anda akan membatasi kode klien dalam nama yang dapat mereka gunakan.
Artinya, jika Anda menetapkan makro di atas (untuk maks), Anda tidak dapat lagi menggunakan
#include <algorithm>
salah satu kode di bawah, kecuali Anda secara eksplisit menulis:#ifdef max #undef max #endif #include <algorithm>
Memiliki makro sebagai ganti variabel / fungsi juga berarti Anda tidak dapat mengambil alamatnya:
jika makro-sebagai-konstanta mengevaluasi ke angka ajaib, Anda tidak dapat meneruskannya dengan alamat
untuk makro-sebagai-fungsi, Anda tidak dapat menggunakannya sebagai predikat atau mengambil alamat fungsi atau memperlakukannya sebagai functor.
Edit: Sebagai contoh, alternatif yang benar di
#define max
atas:template<typename T> inline T max(const T& a, const T& b) { return a > b ? a : b; }
Ini melakukan semua yang dilakukan makro, dengan satu batasan: jika jenis argumen berbeda, versi template memaksa Anda untuk bersikap eksplisit (yang sebenarnya mengarah ke kode yang lebih aman dan lebih eksplisit):
int a = 0; double b = 1.; max(a, b);
Jika maks ini didefinisikan sebagai makro, kode akan dikompilasi (dengan peringatan).
Jika maks ini didefinisikan sebagai fungsi template, kompilator akan menunjukkan ambiguitas, dan Anda harus mengatakan salah satu
max<int>(a, b)
ataumax<double>(a, b)
(dan dengan demikian secara eksplisit menyatakan maksud Anda).sumber
const int someconstant = 437;
, dan dapat digunakan hampir di semua cara penggunaan makro. Begitu juga untuk fungsi kecil. Ada beberapa hal di mana Anda dapat menulis sesuatu sebagai makro yang tidak akan berfungsi dalam ekspresi reguler di C (Anda dapat membuat sesuatu yang rata-rata membuat array dari semua jenis angka, yang tidak dapat dilakukan C - tetapi C ++ memiliki templat untuk itu). Sementara C ++ 11 menambahkan beberapa hal lagi yang "Anda tidak memerlukan makro untuk ini", sebagian besar sudah diselesaikan di C / C ++ sebelumnya.max
danmin
jika diikuti dengan tanda kurung kiri. Tetapi Anda tidak harus menentukan makro seperti itu ...Masalah umum adalah ini:
#define DIV(a,b) a / b printf("25 / (3+2) = %d", DIV(25,3+2));
Ini akan mencetak 10, bukan 5, karena preprocessor akan mengembangkannya dengan cara ini:
printf("25 / (3+2) = %d", 25 / 3 + 2);
Versi ini lebih aman:
#define DIV(a,b) (a) / (b)
sumber
DIV
makro dapat ditulis ulang dengan sepasang () sekitarb
.#define DIV(a,b)
, tidak#define DIV (a,b)
, itu sangat berbeda.#define DIV(a,b) (a) / (b)
tidak cukup baik; sebagai praktik umum, selalu tambahkan tanda kurung terluar, seperti ini:#define DIV(a,b) ( (a) / (b) )
Makro sangat berguna terutama untuk membuat kode generik (parameter makro bisa apa saja), terkadang dengan parameter.
Lebih, kode ini ditempatkan (mis. Dimasukkan) pada titik makro digunakan.
OTOH, hasil serupa dapat dicapai dengan:
fungsi yang kelebihan beban (tipe parameter berbeda)
template, dalam C ++ (tipe dan nilai parameter generik)
fungsi inline (kode tempat di mana mereka dipanggil, alih-alih melompat ke definisi satu titik - namun, ini lebih merupakan rekomendasi untuk kompilator).
edit: tentang mengapa makro buruk:
1) tidak ada pemeriksaan tipe pada argumen (tidak memiliki tipe), sehingga dapat dengan mudah disalahgunakan 2) terkadang berkembang menjadi kode yang sangat kompleks, yang dapat sulit untuk diidentifikasi dan dipahami dalam file yang diproses sebelumnya 3) mudah untuk membuat kesalahan -prone kode di makro, seperti:
#define MULTIPLY(a,b) a*b
dan kemudian menelepon
MULTIPLY(2+3,4+5)
yang berkembang
2 + 3 * 4 + 5 (dan tidak menjadi: (2 + 3) * (4 + 5)).
Untuk mendapatkan yang terakhir, Anda harus menentukan:
#define MULTIPLY(a,b) ((a)*(b))
sumber
Saya rasa tidak ada yang salah dengan menggunakan definisi praprosesor atau makro saat Anda menyebutnya.
Mereka adalah konsep bahasa (meta) yang ditemukan di c / c ++ dan seperti alat lainnya, mereka dapat membuat hidup Anda lebih mudah jika Anda tahu apa yang Anda lakukan. Masalah dengan makro adalah bahwa mereka diproses sebelum kode c / c ++ Anda dan menghasilkan kode baru yang bisa salah dan menyebabkan kesalahan kompiler yang semuanya jelas. Sisi baiknya, mereka dapat membantu Anda menjaga kode tetap bersih dan menghemat banyak pengetikan jika digunakan dengan benar, jadi itu tergantung pada preferensi pribadi.
sumber
Makro di C / C ++ dapat berfungsi sebagai alat penting untuk kontrol versi. Kode yang sama dapat dikirimkan ke dua klien dengan konfigurasi makro kecil. Saya menggunakan hal-hal seperti
#define IBM_AS_CLIENT #ifdef IBM_AS_CLIENT #define SOME_VALUE1 X #define SOME_VALUE2 Y #else #define SOME_VALUE1 P #define SOME_VALUE2 Q #endif
Fungsionalitas semacam ini tidak mudah dilakukan tanpa makro. Makro sebenarnya adalah Alat Manajemen Konfigurasi Perangkat Lunak yang hebat dan bukan hanya cara membuat pintasan untuk penggunaan kembali kode. Menentukan fungsi untuk tujuan dapat digunakan kembali di makro pasti dapat menimbulkan masalah.
sumber
Saya pikir masalahnya adalah bahwa makro tidak dioptimalkan dengan baik oleh kompiler dan "jelek" untuk dibaca dan di-debug.
Seringkali alternatif yang baik adalah fungsi generik dan / atau fungsi inline.
sumber
Makro preprocessor tidak jahat ketika digunakan untuk tujuan yang dimaksudkan seperti:
Alternatif- Seseorang dapat menggunakan beberapa jenis file konfigurasi dalam format ini, xml, json untuk tujuan yang sama. Tetapi menggunakannya akan memiliki efek waktu kerja pada kode yang dapat dihindari oleh makro preprocessor.
sumber