Mengapa makro preprocessor jahat dan apa alternatifnya?

94

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 onceitu 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?

pengguna1849534
sumber
19
#pragmabukan makro.
FooF
1
@ dari direktif preprocessor?
pengguna1849534
6
@ user1849534: Ya, begitulah ... dan nasihat tentang makro tidak membicarakannya #pragma.
Ben Voigt
1
Anda dapat melakukan banyak hal dengan constexpr, inlinefungsi, dan templates, tetapi boost.preprocessordan chaosmenunjukkan bahwa makro memiliki tempatnya. Belum lagi makro konfigurasi untuk kompiler perbedaan, platform, dll.
Brandon
1
kemungkinan duplikat Kapan makro C ++ bermanfaat?
Aaron McDaid

Jawaban:

165

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):

  1. Anda tidak dapat men-debug makro.
  2. Ekspansi makro dapat menyebabkan efek samping yang aneh.
  3. Makro tidak memiliki "namespace", jadi jika Anda memiliki makro yang bentrok dengan nama yang digunakan di tempat lain, Anda mendapatkan penggantian makro di tempat yang tidak Anda inginkan, dan ini biasanya mengarah ke pesan kesalahan yang aneh.
  4. Makro dapat memengaruhi hal-hal yang tidak Anda sadari.

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 enumatauconst 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 kegunaannya x2 = SQUARE(x++). Itu mengarah ke x2 = (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_mallocmalloc 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 memperbarui xdua kali, yang tidak ditentukan dan mungkin akan mengarah pada nilai yang berbeda pada sistem yang berbeda, dan nilai hasil yang berbeda xjuga.

Mats Petersson
sumber
6
Masalahnya if elsebisa diatasi dengan membungkus badan makro di dalamnya do { ... } while(0). Ini berperilaku seperti yang diharapkan sehubungan dengan ifdan fordan 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)
Aaron McDaid
11
@AaronMcDaid: Ya, ada beberapa solusi yang menyelesaikan beberapa masalah yang terpapar di makro ini. Inti dari posting saya bukanlah untuk menunjukkan bagaimana melakukan makro dengan baik, tetapi "betapa mudahnya salah membuat makro", di mana ada alternatif yang baik. Meskipun demikian, ada hal-hal yang diselesaikan makro dengan sangat mudah, dan ada kalanya makro juga merupakan hal yang tepat untuk dilakukan.
Mats Petersson
1
Pada poin 3, kesalahan tidak menjadi masalah lagi. Penyusun modern seperti Clang akan mengatakan sesuatu seperti note: expanded from macro 'begin'dan menunjukkan di mana begindidefinisikan.
kirbyfan64sos
5
Makro sulit diterjemahkan ke bahasa lain.
Marco van de Voort
1
@FrancescoDondi: stackoverflow.com/questions/4176328/… (jawaban itu agak menurun, ini berbicara tentang i ++ * i ++ dan semacamnya.
Mats Petersson
21

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

dengan C ++ 11 baru apakah ada alternatif nyata setelah bertahun-tahun?

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 maxatas:

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)atau max<double>(a, b)(dan dengan demikian secara eksplisit menyatakan maksud Anda).

utnapistim
sumber
1
Itu tidak harus spesifik c ++ 11; Anda cukup menggunakan fungsi untuk mengganti penggunaan makro-sebagai-ekspresi dan [statis] const / constexpr untuk menggantikan penggunaan makro-sebagai-konstanta.
utnapistim
1
Bahkan C99 memungkinkan penggunaan 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.
Mats Petersson
Melakukan kenaikan awal sambil meneruskan argumen adalah praktik pengkodean yang buruk. Dan siapa pun yang mengkode C / C ++ seharusnya tidak menganggap panggilan mirip fungsi bukanlah makro.
StephenG
Banyak implementasi yang secara sukarela memberi tanda kurung pada pengenal maxdan minjika diikuti dengan tanda kurung kiri. Tetapi Anda tidak harus menentukan makro seperti itu ...
LF
14

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)
phaazon.dll
sumber
2
contoh yang menarik, pada dasarnya mereka hanyalah token tanpa semantik
user1849534
Iya. Mereka diperluas seperti yang diberikan pada makro. The DIVmakro dapat ditulis ulang dengan sepasang () sekitar b.
phaazon
2
Maksudmu #define DIV(a,b), tidak #define DIV (a,b), itu sangat berbeda.
rici
6
#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) )
PJTraill
3

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))
pengguna1284631
sumber
3

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.

Sandi Hrvić
sumber
Juga, seperti yang ditunjukkan oleh jawaban lain, definisi preprocessor yang dirancang dengan buruk dapat menghasilkan kode dengan sintaks yang valid tetapi makna semantik berbeda yang berarti bahwa compiler tidak akan mengeluh dan Anda memasukkan bug dalam kode Anda yang akan lebih sulit ditemukan.
Sandi Hrvić
3

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.

indiangarg
sumber
Menyetel nilai Makro pada cmdline selama kompilasi untuk membangun dua varian dari satu basis kode sangat bagus. secukupnya.
kevinf
1
Dari beberapa perspektif, penggunaan ini adalah yang paling berbahaya: alat (IDE, penganalisis statis, pemfaktoran ulang) akan kesulitan menemukan kemungkinan jalur kode.
erenon
1

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.

Davide Icardi
sumber
2
Apa yang membuat Anda percaya bahwa makro tidak dioptimalkan dengan baik? Mereka substitusi teks sederhana, dan hasilnya dioptimalkan seperti halnya kode yang ditulis tanpa makro.
Ben Voigt
@BenVoigt tetapi mereka tidak mempertimbangkan semantik dan ini dapat menyebabkan sesuatu yang dapat dianggap sebagai "tidak-optimal" ... setidaknya ini adalah pemikiran pertama saya tentang stackoverflow.com/a/14041502/1849534
user1849534
1
@ user1849534: Bukan itu yang dimaksud dengan kata "dioptimalkan" dalam konteks kompilasi.
Ben Voigt
1
@BenVoigt Tepatnya, makro hanyalah substitusi teks. Kompiler hanya menggandakan kode, ini bukan masalah kinerja tetapi dapat meningkatkan ukuran program. Terutama benar dalam beberapa konteks di mana Anda memiliki batasan ukuran program. Beberapa kode begitu penuh dengan makro sehingga ukuran programnya menjadi dua kali lipat.
Davide Icardi
1

Makro preprocessor tidak jahat ketika digunakan untuk tujuan yang dimaksudkan seperti:

  • Membuat rilis berbeda dari perangkat lunak yang sama menggunakan konstruksi tipe #ifdef, misalnya rilis jendela untuk wilayah berbeda.
  • Untuk menentukan nilai terkait pengujian kode.

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.

indiangarg
sumber
1
karena C ++ 17 constexpr if + file header yang berisi variabel "config" constexpr dapat menggantikan # ifdef's.
Enhex