Mengapa makro tidak termasuk dalam sebagian besar bahasa pemrograman modern?

31

Saya tahu bahwa mereka diimplementasikan dengan sangat tidak aman di C / C ++. Tidak bisakah mereka diimplementasikan dengan cara yang lebih aman? Apakah kerugian makro benar-benar cukup buruk untuk melebihi kekuatan besar yang mereka berikan?

Casebash
sumber
4
Tepatnya, kekuatan apa yang disediakan makro yang tidak dapat dengan mudah dicapai dengan cara lain?
Chinmay Kanchi
2
Dalam C #, butuh ekstensi bahasa inti untuk membungkus membuat properti sederhana dan bidang dukungan menjadi satu deklarasi. Dan ini membutuhkan lebih dari makro leksikal: bagaimana Anda bisa membuat fitur yang sesuai dengan WithEventspengubah Visual Basic ? Anda membutuhkan sesuatu seperti makro semantik.
Jeffrey Hantin
3
Masalah dengan makro adalah bahwa, seperti semua mekanisme kuat, itu memungkinkan seorang programmer untuk mematahkan asumsi yang dibuat atau akan dibuat oleh orang lain. Asumsi adalah kunci untuk penalaran dan tanpa kemampuan untuk alasan logis tentang logika, membuat kemajuan menjadi penghalang.
dan_waterworth
2
@ Cinmay: makro menghasilkan kode. Tidak ada fitur dalam bahasa Jawa dengan kekuatan itu.
kevin cline
1
@Chinmay Kanchi: Makro memungkinkan untuk mengeksekusi (mengevaluasi) kode pada waktu kompilasi alih-alih pada waktu berjalan.
Giorgio

Jawaban:

57

Saya pikir alasan utamanya adalah bahwa makro adalah leksikal . Ini memiliki beberapa konsekuensi:

  • Kompiler tidak memiliki cara untuk memeriksa bahwa makro ditutup secara semantik, yaitu bahwa ia mewakili "unit makna" seperti fungsi. (Pertimbangkan #define TWO 1+1- apa yang TWO*TWOsama dengan 3.)

  • Makro tidak diketik seperti fungsi. Kompiler tidak dapat memeriksa apakah parameter dan tipe pengembalian masuk akal. Itu hanya dapat memeriksa ekspresi diperluas yang menggunakan makro.

  • Jika kode tidak dikompilasi, kompiler tidak memiliki cara untuk mengetahui apakah kesalahan ada di makro itu sendiri atau tempat di mana makro digunakan. Compiler akan melaporkan tempat yang salah separuh waktu, atau harus melaporkan keduanya meskipun salah satunya mungkin baik-baik saja. (Pertimbangkan #define min(x,y) (((x)<(y))?(x):(y)): Apa yang harus dilakukan kompiler jika jenis xdan ytidak cocok atau tidak diterapkan operator<?)

  • Alat otomatis tidak dapat bekerja dengannya dengan cara yang bermanfaat secara semantik. Secara khusus, Anda tidak dapat memiliki hal-hal seperti IntelliSense untuk makro yang berfungsi seperti fungsi tetapi diperluas ke ekspresi. (Sekali lagi, mincontohnya.)

  • Efek samping dari makro tidak sejelas mereka dengan fungsi, menyebabkan potensi kebingungan bagi programmer. (Pertimbangkan lagi mincontohnya: dalam pemanggilan fungsi, Anda tahu bahwa ekspresi untuk xdievaluasi hanya sekali, tetapi di sini Anda tidak bisa tahu tanpa melihat makro.)

Seperti yang saya katakan, ini semua adalah konsekuensi dari fakta bahwa makro adalah leksikal. Ketika Anda mencoba mengubahnya menjadi sesuatu yang lebih tepat, Anda berakhir dengan fungsi dan konstanta.

Timwi
sumber
32
Tidak semua bahasa makro leksikal. Misalnya, makro Skema sintaksis, dan templat C ++ semantik. Sistem makro leksikal memiliki perbedaan bahwa mereka dapat ditempelkan ke bahasa apa pun tanpa menyadari sintaksnya.
Jeffrey Hantin
8
@ Jeffrey: Saya kira "makro saya leksikal" benar-benar singkatan untuk "ketika orang merujuk ke makro dalam bahasa pemrograman mereka biasanya memikirkan makro leksikal". Sangat disayangkan bahwa Skema akan menggunakan istilah yang dimuat ini untuk sesuatu yang secara fundamental berbeda. Templat C ++, bagaimanapun, tidak banyak disebut sebagai makro, mungkin justru karena mereka tidak sepenuhnya leksikal.
Timwi
25
Saya pikir bahwa penggunaan skema istilah makro tanggal kembali ke Lisp, yang mungkin berarti itu mendahului sebagian besar penggunaan lainnya. IIRC sistem C / C ++ pada awalnya disebut preprocessor .
Bevan
16
@Bevan benar. Mengatakan makro leksikal seperti mengatakan burung tidak bisa terbang karena burung yang paling Anda kenal adalah seekor penguin. Yang mengatakan, sebagian besar (tetapi tidak semua) poin yang Anda naikkan juga berlaku untuk makro sintaksis, meskipun mungkin pada tingkat yang lebih rendah.
Laurence Gonsalves
13

Tapi ya, makro bisa dirancang dan diimplementasikan lebih baik daripada di C / C ++.

Masalah dengan makro adalah bahwa mereka secara efektif mekanisme ekstensi sintaksis bahasa yang menulis ulang kode Anda menjadi sesuatu yang lain.

  • Dalam kasus C / C ++, tidak ada pemeriksaan kewarasan yang mendasar. Jika Anda berhati-hati, semuanya baik-baik saja. Jika Anda melakukan kesalahan, atau jika Anda menggunakan makro secara berlebihan, Anda bisa mendapatkan masalah besar.

    Tambahkan ke ini bahwa banyak hal sederhana yang dapat Anda lakukan dengan makro (gaya C / C ++) dapat dilakukan dengan cara lain dalam bahasa lain.

  • Dalam bahasa lain seperti berbagai dialek Lisp, makro lebih baik diintegrasikan dengan sintaksis bahasa inti, tetapi Anda masih bisa mendapatkan masalah dengan deklarasi dalam "kebocoran" makro. Ini diatasi oleh makro higienis .


Konteks Sejarah Singkat

Makro (kependekan dari instruksi makro) pertama kali muncul dalam konteks bahasa assembly. Menurut Wikipedia , makro tersedia di beberapa perakit IBM pada 1950-an.

LISP asli tidak memiliki makro, tetapi mereka pertama kali diperkenalkan ke MacLisp pada pertengahan 1960-an: https://stackoverflow.com/questions/3065606/when-did-the-ide-of-macros-user-defined-code -transformasi-muncul . http://www.csee.umbc.edu/courses/331/resources/papers/Evolution-of-Lisp.pdf . Sebelum itu, "fexprs" menyediakan fungsionalitas seperti makro.

Versi C paling awal tidak memiliki makro ( http://cm.bell-labs.com/cm/cs/who/dmr/chist.html ). Ini ditambahkan sekitar tahun 1972-73 melalui preprocessor. Sebelum itu, C hanya mendukung #includedan #define.

Makro-preprocessor M4 berasal sekitar tahun 1977.

Bahasa yang lebih baru rupanya menerapkan makro di mana model operasi lebih sintaksis daripada tekstual.

Jadi ketika seseorang berbicara tentang keunggulan definisi khusus dari istilah "makro", penting untuk dicatat bahwa maknanya telah berkembang dari waktu ke waktu.

Stephen C
sumber
9
C ++ diberkati (?) Dengan DUA sistem makro: ekspansi preprocessor dan template.
Jeffrey Hantin
Saya pikir mengklaim bahwa ekspansi template adalah makro sedang memperluas definisi makro. Menggunakan definisi Anda membuat pertanyaan awal tidak berarti dan hanya salah, karena sebagian besar bahasa modern sebenarnya memiliki makro (sesuai dengan definisi Anda). Namun, menggunakan makro sebagai mayoritas pengembang akan menggunakannya membuatnya menjadi pertanyaan yang bagus.
Dunk
@ Dunk, definisi makro seperti dalam Lisp mendahului pemahaman bodoh "modern" seperti dalam preprosesor C yang menyedihkan.
SK-logic
@ SK: Apakah lebih baik untuk "secara teknis" benar dan mengaburkan percakapan atau lebih baik dipahami?
Dunk
1
@Dunk, terminologi tidak dimiliki oleh gerombolan bodoh. Ketidaktahuan mereka seharusnya tidak pernah dipertimbangkan, jika tidak maka akan menyebabkan seluruh domain ilmu komputer mati. Jika seseorang memahami "makro" hanya sebagai referensi ke preprosesor C, itu tidak lain hanyalah ketidaktahuan. Saya ragu ada proporsi yang signifikan dari orang-orang yang tidak pernah mendengar tentang Lisp atau bahkan, pardonnez mon français, "makro" VBA di Kantor.
SK-logic
12

Makro dapat, seperti yang dicatat Scott , memungkinkan Anda menyembunyikan logika. Tentu saja, begitu juga fungsi, kelas, perpustakaan, dan banyak perangkat umum lainnya.

Tetapi sistem makro yang kuat dapat melangkah lebih jauh, memungkinkan Anda untuk merancang dan memanfaatkan sintaks dan struktur yang biasanya tidak ditemukan dalam bahasa tersebut. Ini memang bisa menjadi alat yang luar biasa: bahasa khusus domain, pembuat kode, dan banyak lagi, semua dalam kenyamanan lingkungan bahasa tunggal ...

Namun, itu bisa disalahgunakan. Ini dapat membuat kode lebih sulit untuk dibaca, dipahami, dan didebug, menambah waktu yang diperlukan bagi programmer baru untuk menjadi terbiasa dengan basis kode, dan menyebabkan kesalahan dan penundaan yang mahal.

Jadi untuk bahasa yang dimaksudkan untuk menyederhanakan pemrograman (seperti Java atau Python), sistem seperti itu adalah laknat.

Shog9
sumber
3
Dan kemudian Jawa keluar dari jalur dengan menambahkan foreach, anotasi, pernyataan, obat generik, ...
Jeffrey Hantin
2
@ Jeffrey: dan jangan lupa, banyak generator kode pihak ke-3 . Setelah dipikir-pikir, mari kita lupakan itu.
Shog9
4
Contoh lain tentang bagaimana Java lebih sederhana daripada sederhana.
Jeffrey Hantin
3
@ JeffreyHantin: Apa yang salah dengan foreach?
Casebash
1
@Dunk Analogi ini mungkin peregangan ... tapi saya pikir diperpanjang bahasa seperti plutonium. Mahal untuk diterapkan, sangat sulit untuk mesin ke bentuk yang diinginkan, dan sangat berbahaya jika disalahgunakan, tetapi juga luar biasa kuat bila diterapkan dengan baik.
Jeffrey Hantin
6

Makro dapat diimplementasikan dengan sangat aman dalam beberapa keadaan - di Lisp misalnya, makro hanyalah fungsi yang mengembalikan kode yang diubah sebagai struktur data (s-ekspresi). Tentu saja, Lisp mendapat manfaat signifikan dari fakta bahwa itu homoikonik dan fakta bahwa "kode adalah data".

Contoh betapa mudahnya makro adalah contoh Clojure ini yang menentukan nilai default untuk digunakan dalam kasus pengecualian:

(defmacro on-error [default-value code]
  `(try ~code (catch Exception ~'e ~default-value)))

(on-error 0 (+ nil nil))               ;; would normally throw NullPointerException
=> 0                                   ;l; but we get the default value

Bahkan di Lisps sekalipun, saran umum adalah "jangan gunakan makro kecuali Anda harus".

Jika Anda tidak menggunakan bahasa homoikonik, maka makro menjadi lebih rumit dan berbagai opsi lain semuanya memiliki beberapa jebakan:

  • Makro berbasis teks - misalnya preproses C - mudah diimplementasikan tetapi sangat sulit digunakan dengan benar karena Anda perlu menghasilkan sintaksis sumber yang benar dalam bentuk tekstual, termasuk setiap sintaksis sintaksis.
  • DSLS berbasis makro - misalnya sistem template C ++. Kompleks, dapat dengan sendirinya menghasilkan beberapa sintaks yang rumit, dapat menjadi sangat kompleks bagi kompiler dan penulis alat untuk menangani dengan benar karena memperkenalkan kompleksitas baru yang signifikan ke dalam sintaksis bahasa dan semantik.
  • API manipulasi AST / bytecode - mis. Pembuatan Java refleksi / bytecode - secara teoritis sangat fleksibel tetapi bisa menjadi sangat bertele-tele: memerlukan banyak kode untuk melakukan hal-hal yang cukup sederhana. Jika dibutuhkan sepuluh baris kode untuk menghasilkan yang setara dengan fungsi tiga baris, maka Anda belum mendapatkan banyak dengan upaya meta-pemrograman ...

Selain itu, segala sesuatu yang makro dapat lakukan pada akhirnya dapat dicapai dalam beberapa cara lain dalam bahasa lengkap turing (bahkan jika ini berarti menulis banyak boilerplate). Sebagai hasil dari semua trickiness ini, tidak mengherankan bahwa banyak bahasa memutuskan bahwa makro tidak benar-benar sepadan dengan semua upaya untuk diimplementasikan.

mikera
sumber
Ugh. Tidak lebih "kode adalah data adalah hal yang baik" sampah. Kode adalah kode, data adalah data, dan gagal memisahkan keduanya dengan benar adalah lubang keamanan. Anda akan berpikir bahwa kemunculan SQL Injection sebagai salah satu kelas kerentanan terbesar yang ada akan mendiskreditkan gagasan membuat kode dan data dengan mudah dipertukarkan sekali dan untuk semua.
Mason Wheeler
10
@ Alasan - Saya tidak berpikir Anda mengerti konsep kode-adalah-data. Semua kode sumber program C juga data - kebetulan diekspresikan dalam format teks. Lisps adalah sama, kecuali mereka mengekspresikan kode dalam struktur data menengah praktis (s-expressions) yang memungkinkannya untuk dimanipulasi dan diubah oleh makro sebelum dikompilasi. Dalam kedua kasus mengirim input yang tidak dipercaya ke kompiler bisa menjadi lubang keamanan - tetapi itu sulit dilakukan secara tidak sengaja dan itu akan menjadi kesalahan Anda karena melakukan sesuatu yang bodoh, bukan kompiler.
mikera
Rust adalah sistem makro aman lain yang menarik. Ini memiliki aturan ketat / semantik untuk menghindari jenis masalah yang terkait dengan makro leksikal C / C ++, dan menggunakan DSL untuk menangkap bagian dari ekspresi input ke dalam variabel makro yang akan dimasukkan kemudian. Ini tidak sekuat kemampuan Lisps untuk memanggil fungsi apa pun pada input, tetapi itu memberikan sejumlah besar makro manipulasi yang berguna yang dapat dipanggil dari makro lain.
zstewart
Ugh. Tidak ada lagi sampah keamanan . Jika tidak, salahkan setiap sistem dengan arsitektur von Neumann bawaan, misalnya setiap prosesor IA-32 dengan kemampuan kode modifikasi diri. Saya menduga apakah Anda dapat mengisi lubang secara fisik ... Bagaimanapun, Anda harus menghadapi kenyataan secara umum: isomorfisme antara kode dan data adalah sifat dunia dalam beberapa cara . Dan itu (mungkin) Anda, sang programmer, yang bertugas menjaga invariansi persyaratan keamanan, yang tidak sama dengan menerapkan pemisahan prematur secara buatan di mana saja.
FrankHB
4

Untuk menjawab pertanyaan Anda, pikirkan makro apa yang dominan digunakan untuk (Peringatan: kode yang dikompilasi otak).

  • Makro digunakan untuk mendefinisikan konstanta simbolik #define X 100

Ini dapat dengan mudah diganti dengan: const int X = 100;

  • Makro digunakan untuk mendefinisikan (pada dasarnya) fungsi agnostik tipe inline #define max(X,Y) (X>Y?X:Y)

Dalam bahasa apa pun yang mendukung kelebihan fungsi, ini dapat ditiru dengan cara yang jauh lebih aman dengan memiliki fungsi berlebih dari jenis yang benar, atau, dalam bahasa yang mendukung obat generik, dengan fungsi generik. Makro akan dengan senang hati mencoba membandingkan apa pun termasuk pointer atau string, yang mungkin dikompilasi, tetapi hampir pasti bukan yang Anda inginkan. Di sisi lain, jika Anda membuat macro-safe, mereka tidak menawarkan manfaat atau kenyamanan kelebihan fungsi.

  • Makro digunakan untuk menentukan pintasan ke elemen yang sering digunakan. #define p printf

Ini mudah diganti oleh fungsi p()yang melakukan hal yang sama. Ini cukup terlibat dalam C (mengharuskan Anda untuk menggunakan va_arg()keluarga fungsi) tetapi dalam banyak bahasa lain yang mendukung sejumlah variabel argumen fungsi, itu jauh lebih sederhana.

Mendukung fitur-fitur ini dalam bahasa daripada dalam bahasa makro khusus lebih sederhana, lebih sedikit kesalahan, dan jauh lebih membingungkan bagi orang lain yang membaca kode. Bahkan, saya tidak bisa memikirkan satu kasus penggunaan untuk makro yang tidak dapat dengan mudah diduplikasi dengan cara lain. Satu- satunya tempat di mana makro benar-benar berguna adalah ketika mereka terikat pada konstruksi kompilasi bersyarat seperti #if(dll.).

Pada titik itu, saya tidak akan berdebat dengan Anda, karena saya percaya bahwa solusi non-preprosesor untuk kompilasi bersyarat dalam bahasa populer sangat rumit (seperti injeksi bytecode di Jawa). Tetapi bahasa seperti D telah datang dengan solusi yang tidak memerlukan preprosesor dan tidak lebih rumit daripada menggunakan kondisional preprosesor, sementara jauh lebih sedikit rawan kesalahan.

Chinmay Kanchi
sumber
1
jika Anda harus # mendefinisikan maks, silakan letakkan tanda kurung di sekitar parameter, sehingga tidak ada efek yang tidak terduga dari prioritas operator ... seperti # definisi maks (X, Y) ((X)> (Y)? (X) :( Y))
foo
Anda benar-benar menyadari bahwa itu hanyalah sebuah contoh ... Maksudnya adalah untuk menggambarkan.
Chinmay Kanchi
+1 untuk masalah sistematis ini. Saya ingin menambahkan bahwa kompilasi bersyarat dapat dengan mudah menjadi mimpi buruk - Saya ingat bahwa ada sebuah program bernama "unifdef" (??) yang tujuannya adalah membuatnya terlihat apa yang tersisa setelah postprocessing.
Ingo
7
In fact, I can't think of a single use-case for macros that can't easily be duplicated in another way: di C setidaknya, Anda tidak bisa membentuk pengidentifikasi (nama variabel) menggunakan token concatenation tanpa menggunakan macro.
Charles Salvia
Makro memungkinkan untuk memastikan bahwa kode dan struktur data tertentu tetap "paralel". Misalnya, jika ada sejumlah kecil kondisi dengan pesan terkait, dan seseorang harus bertahan dalam format ringkas, mencoba menggunakan a enumuntuk menentukan kondisi dan array string konstan untuk menentukan pesan dapat mengakibatkan masalah jika enum dan array keluar dari sinkronisasi. Menggunakan makro untuk mendefinisikan semua pasangan (enum, string) dan kemudian memasukkan makro itu dua kali dengan definisi yang tepat lainnya dalam lingkup setiap kali akan membiarkan satu menempatkan setiap nilai enum di sebelah stringnya.
supercat
2

Masalah terbesar yang saya lihat dengan makro adalah bahwa ketika banyak digunakan mereka dapat membuat kode sangat sulit untuk dibaca dan dipelihara karena mereka memungkinkan Anda untuk menyembunyikan logika di makro yang mungkin atau mungkin tidak mudah ditemukan (dan mungkin atau mungkin tidak sepele) ).

Scott Dorman
sumber
Bukankah makro harus didokumentasikan?
Casebash
@Casebash: Tentu saja, makro harus didokumentasikan sama seperti kode sumber lainnya ... tetapi dalam praktiknya saya jarang melihatnya selesai.
Scott Dorman
3
Pernah men-debug dokumentasi? Itu tidak cukup ketika berhadapan dengan kode yang ditulis dengan buruk.
JeffO
OOP memiliki beberapa masalah itu juga ...
aoeu256
2

Jangan mulai dengan mencatat bahwa MACRO di C / C ++ sangat terbatas, rawan kesalahan, dan tidak terlalu berguna.

MACRO seperti yang diimplementasikan dalam say LISP, atau bahasa assembler z / OS dapat diandalkan dan sangat berguna.

Tetapi karena penyalahgunaan fungsi terbatas dalam C mereka telah mengumpulkan reputasi buruk. Jadi tidak ada yang mengimplementasikan makro lagi, alih-alih Anda mendapatkan hal-hal seperti Template yang melakukan beberapa hal-hal sederhana yang biasa dilakukan makro dan hal-hal seperti penjelasan Java yang melakukan beberapa hal yang lebih rumit yang biasa dilakukan makro.

James Anderson
sumber