Dlaczego makra nie są zawarte w większości współczesnych języków programowania?

31

Wiem, że są one wyjątkowo niebezpiecznie implementowane w C / C ++. Czy nie można ich wdrożyć w bezpieczniejszy sposób? Czy wady makr są na tyle poważne, że przewyższają ich ogromną moc?

Casebash
źródło
4
Jaką dokładnie moc zapewniają makra, których nie da się łatwo osiągnąć w inny sposób?
Chinmay Kanchi,
2
W języku C # rozszerzenie podstawowego języka zajęło utworzenie prostego pola właściwości i kopii zapasowej w jednej deklaracji. A to wymaga czegoś więcej niż makr leksykalnych: jak stworzyć funkcję odpowiadającą WithEventsmodyfikatorowi Visual Basic ? Potrzebujesz czegoś takiego jak makra semantyczne.
Jeffrey Hantin
3
Problem z makrami polega na tym, że podobnie jak wszystkie potężne mechanizmy, pozwala programiście przełamać założenia, które inni poczynili lub zrobią. Założenia są kluczem do rozumowania i bez możliwości rozsądnego rozumowania logiki, postępy stają się zabronione.
dan_waterworth
2
@Chinmay: makra generują kod. Z taką mocą nie ma funkcji w języku Java.
kevin cline
1
@Chinmay Kanchi: Makra pozwalają na wykonanie (ocenę) kodu w czasie kompilacji zamiast w czasie wykonywania.
Giorgio

Odpowiedzi:

57

Myślę, że głównym powodem jest to, że makra są leksykalne . Ma to kilka konsekwencji:

  • Kompilator nie ma możliwości sprawdzenia, czy makro jest semantycznie zamknięte, tj. Czy reprezentuje „jednostkę znaczenia”, jak funkcja. (Zastanów się #define TWO 1+1- co się TWO*TWOrówna?)

  • Makra nie są pisane tak jak funkcje. Kompilator nie może sprawdzić, czy parametry i typ zwrotu mają sens. Może sprawdzać tylko rozwinięte wyrażenie, które korzysta z makra.

  • Jeśli kod nie zostanie skompilowany, kompilator nie będzie wiedział, czy błąd występuje w samym makrze, czy w miejscu, w którym makro jest używane. Kompilator albo zgłosi nieprawidłowe miejsce przez połowę czasu, albo musi zgłosić oba, nawet jeśli jeden z nich jest prawdopodobnie w porządku. (Zastanów się #define min(x,y) (((x)<(y))?(x):(y)): co powinien zrobić kompilator, jeśli typy xi ynie pasują lub nie implementują operator<?)

  • Zautomatyzowane narzędzia nie mogą z nimi współpracować w semantycznie użyteczny sposób. W szczególności nie możesz mieć takich makr, jak IntelliSense dla makr, które działają jak funkcje, ale rozwijają się do wyrażenia. (Ponownie minprzykład).

  • Skutki uboczne makra nie są tak wyraźne, jak w przypadku funkcji, powodując potencjalne zamieszanie dla programisty. (Ponownie rozważ minprzykład: w wywołaniu funkcji wiesz, że wyrażenie dla xjest oceniane tylko raz, ale tutaj nie możesz wiedzieć bez spojrzenia na makro.)

Tak jak powiedziałem, są to wszystkie konsekwencje faktu, że makra są leksykalne. Kiedy próbujesz zamienić je w coś bardziej właściwego, otrzymujesz funkcje i stałe.

Timwi
źródło
32
Nie wszystkie języki makr są leksykalne. Na przykład makra schematu są składniowe, a szablony C ++ są semantyczne. Makrosystemy leksykalne wyróżniają się tym, że można je przypisać do dowolnego języka bez wiedzy o jego składni.
Jeffrey Hantin
8
@Jeffrey: Wydaje mi się, że moje „makra są leksykalne” są naprawdę skrótem dla „kiedy ludzie odnoszą się do makr w języku programowania, zwykle myślą o makrach leksykalnych”. Szkoda, że ​​Schemat użył tego załadowanego terminu na coś, co jest zasadniczo inne. Jednak szablony C ++ nie są powszechnie nazywane makrami, prawdopodobnie właśnie dlatego, że nie są całkowicie leksykalne.
Timwi
25
Myślę, że użycie przez makro terminu Makro pochodzi od Lispa, co prawdopodobnie oznacza, że ​​wyprzedza większość innych zastosowań. IIRC system C / C ++ był pierwotnie nazywany preprocesorem .
Bevan,
16
@Bevan ma rację. Mówienie, że makra są leksykalne, przypomina, że ​​ptaki nie mogą latać, ponieważ ptak, którego najbardziej znasz, to pingwin. To powiedziawszy, większość (ale nie wszystkie) kwestie, które poruszasz, dotyczą również makr syntaktycznych, choć może w mniejszym stopniu.
Laurence Gonsalves,
13

Ale tak, makra można zaprojektować i zaimplementować lepiej niż w C / C ++.

Problem z makrami polega na tym, że są one w rzeczywistości mechanizmem rozszerzenia składni języka , który przepisuje kod na coś innego.

  • W przypadku C / C ++ nie ma fundamentalnego sprawdzania rozsądku. Jeśli jesteś ostrożny, wszystko jest w porządku. Jeśli popełnisz błąd lub nadmiernie użyjesz makr, możesz wpaść w poważne problemy.

    Dodaj do tego, że wiele prostych rzeczy, które możesz zrobić za pomocą makr (w stylu C / C ++), można wykonać na inne sposoby w innych językach.

  • W innych językach, takich jak różne dialekty Lisp, makra są lepiej zintegrowane ze składnią języka podstawowego, ale nadal możesz mieć problemy z deklaracjami w makrze „wyciekające”. To jest skierowana przez higienicznych makr .


Krótki kontekst historyczny

Makra (skrót od makropoleceń) pojawiły się po raz pierwszy w kontekście języka asemblera. Według Wikipedii makra były dostępne w niektórych asemblerach IBM w latach 50.

Oryginalny LISP nie miał makr, ale po raz pierwszy został wprowadzony do MacLisp w połowie lat 60. XX wieku: https://stackoverflow.com/questions/3065606/when-did-the-idea-of-macros-user-defined-code -transformacja-pojawiają się . http://www.csee.umbc.edu/courses/331/resources/papers/Evolution-of-Lisp.pdf . Wcześniej „fexprs” zapewniał funkcjonalność podobną do makra.

Pierwsze wersje C nie miały makr ( http://cm.bell-labs.com/cm/cs/who/dmr/chist.html ). Zostały one dodane około 1972–73 za pośrednictwem preprocesora. Wcześniej C obsługiwał tylko #includei #define.

Makroprocesor M4 powstał około 1977 r.

Nowsze języki najwyraźniej implementują makra, w których model działania jest bardziej składniowy niż tekstowy.

Kiedy więc ktoś mówi o prymacie konkretnej definicji terminu „makro”, należy zauważyć, że znaczenie to ewoluowało z czasem.

Stephen C.
źródło
9
C ++ jest błogosławiony (?) Z DWIMI systemami makr: preprocesorem i rozszerzeniem szablonu.
Jeffrey Hantin
Myślę, że twierdzenie, że rozwinięcie szablonu jest makrem, rozszerza definicję makra. Użycie definicji powoduje, że pierwotne pytanie jest pozbawione sensu i po prostu jest błędne, ponieważ większość współczesnych języków faktycznie ma makra (zgodnie z definicją). Jednak użycie makro jako przeważającej większości programistów użyłoby go, co stanowi dobre pytanie.
Dunk
@Dunk, definicja makra jak w Lisp poprzedza „nowoczesne” głupie rozumienie jak w żałosnym preprocesorze C.
SK-logic
@SK: Czy lepiej jest być „technicznie” poprawnym i zaciemniać rozmowę, czy lepiej być rozumianym?
Dunk
1
@Dunk, terminologia nie jest własnością ignoranckich hord. Ich ignorancja nigdy nie powinna być brana pod uwagę, w przeciwnym razie doprowadzi to do ogłuszenia całej dziedziny informatyki. Jeśli ktoś rozumie „makro” jedynie jako odniesienie do preprocesora C, jest to tylko ignorancja. Wątpię, aby istniał znaczny odsetek ludzi, którzy nigdy nie słyszeli o Lisp ani nawet o pardonnez mon français, „makrach” VBA w pakiecie Office.
SK-logic
12

Makra mogą, jak zauważa Scott , pozwolić ukryć logikę. Oczywiście, podobnie jak funkcje, klasy, biblioteki i wiele innych popularnych urządzeń.

Potężny system makr może jednak pójść dalej, umożliwiając projektowanie i wykorzystywanie składni i struktur normalnie nie spotykanych w języku. To może być naprawdę wspaniałe narzędzie: języki specyficzne dla domeny, generatory kodów i inne, wszystko w komfortowym środowisku jednego języka ...

Można go jednak nadużywać. Może to utrudnić czytanie, zrozumienie i debugowanie kodu, wydłużyć czas potrzebny nowym programistom na zapoznanie się z bazą kodu oraz prowadzić do kosztownych błędów i opóźnień.

Tak więc w przypadku języków, które mają uprościć programowanie (takich jak Java lub Python), taki system jest anatemą.

Shog9
źródło
3
A potem Java
zjechała
2
@Jeffrey: i nie zapominajmy, że jest wiele generatorów kodów innych firm . Po namyśle zapomnijmy o nich.
Shog9
4
Kolejny przykład tego, jak Java była uproszczona, a nie prosta.
Jeffrey Hantin,
3
@JeffreyHantin: Co jest złego w Foreach?
Casebash,
1
@Dunk Ta analogia może być rozciągnięta ... ale myślę, że rozszerzalność języka jest jak pluton. Kosztowne w implementacji, bardzo trudne do obróbki w pożądane kształty i niezwykle niebezpieczne, jeśli są niewłaściwie używane, ale również niezwykle wydajne, gdy są dobrze stosowane.
Jeffrey Hantin
6

Makra mogą być implementowane bardzo bezpiecznie w niektórych okolicznościach - na przykład w Lisp, makra to tylko funkcje, które zwracają przekształcony kod jako strukturę danych (wyrażenie-s). Oczywiście Lisp w znacznym stopniu korzysta z faktu, że jest homoiczny i że „kod to dane”.

Przykładem tego, jak łatwe mogą być makra, jest przykład Clojure, który określa domyślną wartość do zastosowania w przypadku wyjątku:

(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

Jednak nawet w Lisps ogólna rada brzmi: „nie używaj makr, chyba że musisz”.

Jeśli nie używasz języka homoiconic, makra stają się znacznie trudniejsze, a różne inne opcje mają pewne pułapki:

  • Makra tekstowe - np. Preprocesor C - proste do wdrożenia, ale bardzo trudne w użyciu, ponieważ należy wygenerować poprawną składnię źródłową w formie tekstowej, w tym wszelkie dziwactwa składniowe
  • Makro DSLS - np. System szablonów C ++. Złożona, sama w sobie może powodować trudną składnię, może być niezwykle złożona dla kompilatorów i twórców narzędzi do prawidłowej obsługi, ponieważ wprowadza znaczną nową złożoność do składni języka i semantyki.
  • Interfejsy API do manipulowania kodem AST / bajtowym - np. Odbicie Java / generowanie kodu bajtowego - teoretycznie bardzo elastyczne, ale mogą być bardzo szczegółowe: może wymagać dużo kodu, aby wykonać dość proste rzeczy. Jeśli potrzeba dziesięciu wierszy kodu, aby wygenerować odpowiednik funkcji trzywierszowej, to niewiele zyskałeś dzięki swoim meta-programowaniu ...

Co więcej, wszystko, co makro może zrobić, może zostać ostatecznie osiągnięte w inny sposób w pełnym języku turinga (nawet jeśli oznacza to napisanie dużej liczby szablonów). W wyniku całej tej podstępności nic dziwnego, że wiele języków decyduje, że makra nie są warte tyle wysiłku, aby je wdrożyć.

mikera
źródło
Ugh. Nie więcej „kodu to dane to dobra rzecz” śmieci. Kod to kod, dane to dane, a ich niepoprawne rozdzielenie stanowi lukę w zabezpieczeniach. Można by pomyśleć, że pojawienie się SQL Injection jako jednej z największych istniejących klas podatności zdyskredytowałoby pomysł łatwej wymiany kodu i danych raz na zawsze.
Mason Wheeler,
10
@Mason - Nie sądzę, że rozumiesz pojęcie kod-dane-dane. Cały kod źródłowy programu C to także dane - akurat jest wyrażany w formacie tekstowym. Lisps są takie same, z tym wyjątkiem, że wyrażają kod w praktycznych pośrednich strukturach danych (wyrażeniach s), które umożliwiają jego manipulację i transformację przez makra przed kompilacją. W obu przypadkach wysłanie niezaufanego wejścia do kompilatora może stanowić lukę w zabezpieczeniach - ale jest to trudne do wykonania przypadkowo i byłaby to twoja wina za zrobienie czegoś głupiego, a nie kompilatora.
mikera
Rdza to kolejny interesujący bezpieczny system makr. Ma ścisłe reguły / semantykę, aby uniknąć problemów związanych z makrami leksykalnymi C / C ++, i używa DSL do przechwytywania części wyrażenia wejściowego do makrozmiennych, które zostaną później wstawione. Nie jest tak potężny, jak zdolność Lisps do wywoływania dowolnej funkcji na wejściu, ale zapewnia duży zestaw przydatnych makr manipulacji, które można wywoływać z innych makr.
zstewart
Ugh. Nie więcej śmieci bezpieczeństwa . W przeciwnym razie obwiniaj każdy system z wbudowaną architekturą von Neumanna, np. Każdy procesor IA-32 z możliwością samodzielnego modyfikowania kodu. Podejrzewam, czy możesz wypełnić dziurę fizycznie ... W każdym razie musisz zmierzyć się z faktem: izomorfizm między kodem a danymi jest naturą świata na kilka sposobów . I to (prawdopodobnie) ty, programista, masz obowiązek zachowania niezmienności wymagań bezpieczeństwa, co nie jest tym samym, co sztuczne stosowanie przedwczesnej segregacji w dowolnym miejscu.
FrankHB
4

Aby odpowiedzieć na pytania, zastanów się, do jakich makr używa się głównie (Ostrzeżenie: kod skompilowany z mózgiem).

  • Makra używane do definiowania stałych symbolicznych #define X 100

Można to łatwo zastąpić: const int X = 100;

  • Makra używane do definiowania (zasadniczo) wbudowanych funkcji agnostycznych #define max(X,Y) (X>Y?X:Y)

W każdym języku, który obsługuje przeciążanie funkcji, można to emulować w sposób znacznie bardziej bezpieczny dla typu, przeciążając funkcje poprawnego typu lub, w języku obsługującym funkcje ogólne, funkcją ogólną. Makro chętnie spróbuje porównać wszystko, w tym wskaźniki lub ciągi, które mogą się skompilować, ale prawie na pewno nie jest to, czego chciałeś. Z drugiej strony, jeśli makra są bezpieczne dla typu, nie oferują żadnych korzyści ani wygody w przypadku przeciążonych funkcji.

  • Makra używane do określania skrótów do często używanych elementów. #define p printf

Można to łatwo zastąpić funkcją, p()która robi to samo. Jest to dość zaangażowane w C (wymagający użycia va_arg()rodziny funkcji), ale w wielu innych językach, które obsługują zmienną liczbę argumentów funkcji, jest to o wiele prostsze.

Obsługa tych funkcji w języku zamiast w specjalnym języku makr jest prostsza, mniej podatna na błędy i znacznie mniej myląca dla osób czytających kod. W rzeczywistości nie mogę wymyślić jednego przypadku użycia makr, którego nie można łatwo powielić w inny sposób. Tylko miejsce, gdzie makra są naprawdę przydatna jest, gdy są one związane z konstrukcjami warunkowego składankach takich jak #if(itd.).

W tej kwestii nie będę się z tobą kłócił, ponieważ uważam, że niepreprocesorowe rozwiązania kompilacji warunkowej w popularnych językach są wyjątkowo kłopotliwe (jak wstrzykiwanie kodu bajtowego w Javie). Ale języki takie jak D wymyślają rozwiązania, które nie wymagają preprocesora i nie są bardziej kłopotliwe niż stosowanie warunkowych preprocesorów, a jednocześnie są znacznie mniej podatne na błędy.

Chinmay Kanchi
źródło
1
jeśli musisz # zdefiniować maksimum, umieść nawiasy wokół parametrów, aby nie było nieoczekiwanych efektów z pierwszeństwa operatora ... jak # zdefiniować max (X, Y) ((X)> (Y)? (X) :( Y))
foo
Zdajesz sobie sprawę, że to był tylko przykład ... Zamiarem było zilustrowanie.
Chinmay Kanchi
+1 za systematyczne rozpatrywanie sprawy. Lubię dodawać, że kompilacja warunkowa może z łatwością stać się koszmarem - pamiętam, że istniał program o nazwie „unifdef” (??), którego celem było uwidocznienie tego, co pozostało po przetworzeniu.
Ingo
7
In fact, I can't think of a single use-case for macros that can't easily be duplicated in another way: przynajmniej w C nie można tworzyć identyfikatorów (nazw zmiennych) przy użyciu łączenia znaczników bez użycia makr.
Charles Salvia,
Makra pozwalają zapewnić, że pewne struktury kodu i danych pozostają „równoległe”. Na przykład, jeśli istnieje niewielka liczba warunków z powiązanymi komunikatami i trzeba będzie je utrwalić w zwięzłym formacie, próba użycia enumdo zdefiniowania warunków i stałej tablicy ciągów do zdefiniowania komunikatów może spowodować problemy, jeśli wyliczanie i tablica nie są zsynchronizowane. Użycie makra do zdefiniowania wszystkich par (wyliczenie, ciąg), a następnie dwukrotne włączenie tego makra z innymi właściwymi definicjami w zakresie za każdym razem pozwoliłoby na umieszczenie każdej wartości wyliczenia obok łańcucha.
supercat,
2

Największym problemem, jaki widziałem w przypadku makr, jest to, że gdy są intensywnie używane, mogą bardzo utrudniać odczytanie i utrzymanie kodu, ponieważ pozwalają ukryć logikę w makrze, które może być lub nie być łatwe do znalezienia (i może być trywialne ).

Scott Dorman
źródło
Czy makro nie powinno być dokumentowane?
Casebash,
@Casebash: Oczywiście makro powinno być udokumentowane tak jak każdy inny kod źródłowy ... ale w praktyce rzadko go widziałem.
Scott Dorman,
3
Czy kiedykolwiek debugowałeś dokumentację? To po prostu za mało, gdy mamy do czynienia ze źle napisanym kodem.
JeffO
OOP ma też niektóre z tych problemów ...
aoeu256
2

Nie zacznijmy od zauważenia, że ​​MAKRO w C / C ++ są bardzo ograniczone, podatne na błędy i niezbyt przydatne.

MAKRO zaimplementowane w powiedzmy LISP lub asembler z / OS mogą być niezawodne i niezwykle przydatne.

Ale z powodu nadużycia ograniczonej funkcjonalności w C mają złą reputację. Więc nikt już nie implementuje makr, zamiast tego dostajesz takie szablony, jak niektóre proste makra, takie jak adnotacje Javy, które wykonują niektóre bardziej złożone makra.

James Anderson
źródło