Warum sind Makros in den meisten modernen Programmiersprachen nicht enthalten?

31

Ich weiß, dass sie in C / C ++ extrem unsicher implementiert sind. Können sie nicht sicherer umgesetzt werden? Sind die Nachteile von Makros wirklich schlimm genug, um die enorme Leistung, die sie bieten, aufzuwiegen?

Casebash
quelle
4
Welche Leistung bieten Makros, die auf andere Weise nicht so einfach zu erreichen sind?
Chinmay Kanchi
2
In C # war eine Kernsprache erforderlich, um das Erstellen einer einfachen Eigenschaft und eines Sicherungsfelds in eine Deklaration zu integrieren. Und dies erfordert mehr als nur lexikalische Makros: Wie können Sie eine Funktion erstellen, die dem WithEventsModifikator von Visual Basic entspricht ? Sie brauchen so etwas wie semantische Makros.
Jeffrey Hantin
3
Das Problem mit Makros ist, dass es einem Programmierer wie alle leistungsfähigen Mechanismen ermöglicht, die Annahmen zu brechen, die andere gemacht haben oder machen werden. Annahmen sind der Schlüssel zum Denken und ohne die Fähigkeit, über Logik nachzudenken, wird es unmöglich, Fortschritte zu erzielen.
Dan_waterworth
2
@Chinmay: Makros generieren Code. Es gibt keine Funktion in der Java-Sprache mit dieser Leistung.
Kevin Cline
1
@Chinmay Kanchi: Makros ermöglichen das Ausführen (Auswerten) von Code zur Kompilierungszeit anstatt zur Laufzeit.
Giorgio

Antworten:

57

Ich denke, der Hauptgrund ist, dass Makros lexikalisch sind . Dies hat mehrere Konsequenzen:

  • Der Compiler kann nicht überprüfen, ob ein Makro semantisch geschlossen ist, dh, dass es wie eine Funktion eine „Bedeutungseinheit“ darstellt. (Überlegen Sie #define TWO 1+1- was macht TWO*TWOgleich? 3.)

  • Makros werden nicht wie Funktionen getippt. Der Compiler kann nicht überprüfen, ob die Parameter und der Rückgabetyp sinnvoll sind. Es kann nur den erweiterten Ausdruck überprüfen, der das Makro verwendet.

  • Wenn der Code nicht kompiliert wird, kann der Compiler nicht wissen, ob der Fehler im Makro selbst oder an der Stelle vorliegt, an der das Makro verwendet wird. Der Compiler meldet entweder die Hälfte der Zeit die falsche Stelle oder er muss beide melden, obwohl eine davon wahrscheinlich in Ordnung ist. (Überlegen Sie #define min(x,y) (((x)<(y))?(x):(y)): Was sollte der Compiler tun, wenn die Typen von xund ynicht übereinstimmen oder nicht implementiert werden operator<?)

  • Automatisierte Tools können nicht mit ihnen auf semantisch nützliche Weise arbeiten. Insbesondere können Sie keine Funktionen wie IntelliSense für Makros verwenden, die wie Funktionen funktionieren, jedoch zu einem Ausdruck erweitert werden. (Wieder das minBeispiel.)

  • Die Nebenwirkungen eines Makros sind nicht so deutlich wie bei Funktionen, was für den Programmierer zu Verwirrung führen kann. (Betrachten Sie noch einmal das minBeispiel: In einem Funktionsaufruf wissen Sie, dass der Ausdruck für xnur einmal ausgewertet wird, aber hier können Sie es nicht wissen, ohne das Makro zu betrachten.)

Wie gesagt, das sind alles Konsequenzen der Tatsache, dass Makros lexikalisch sind. Wenn Sie versuchen, sie in etwas Richtigeres zu verwandeln, erhalten Sie Funktionen und Konstanten.

Timwi
quelle
32
Nicht alle Makrosprachen sind lexikalisch. Zum Beispiel sind Schema-Makros syntaktisch und C ++ - Vorlagen semantisch. Lexikalische Makrosysteme zeichnen sich dadurch aus, dass sie auf jede Sprache angewendet werden können, ohne deren Syntax zu kennen.
Jeffrey Hantin
8
@ Jeffrey: Ich denke, meine "Makros sind lexikalisch" ist eine Abkürzung für "Wenn Leute in einer Programmiersprache auf Makros verweisen, denken sie im Allgemeinen an lexikalische Makros". Es ist bedauerlich, dass Scheme diesen geladenen Begriff für etwas verwenden würde, das grundlegend anders ist. C ++ - Vorlagen werden jedoch nicht allgemein als Makros bezeichnet, vermutlich gerade weil sie nicht vollständig lexikalisch sind.
Timwi
25
Ich denke, dass Schemes Verwendung des Begriffs Makro auf Lisp zurückgeht, was wahrscheinlich bedeutet, dass es vor den meisten anderen Verwendungen liegt. IIRC Das C / C ++ - System wurde ursprünglich als Präprozessor bezeichnet .
Bevan
16
@Bevan ist richtig. Zu sagen, Makros seien lexikalisch, ist wie zu sagen, Vögel könnten nicht fliegen, weil der Vogel, mit dem Sie am besten vertraut sind, ein Pinguin ist. Die meisten (aber nicht alle) Punkte, die Sie ansprechen, gelten jedoch auch für syntaktische Makros, wenn auch in geringerem Maße.
Laurence Gonsalves
13

Aber ja, Makros können besser entworfen und implementiert werden als in C / C ++.

Das Problem bei Makros besteht darin, dass es sich tatsächlich um einen Mechanismus zur Erweiterung der Sprachsyntax handelt , der Ihren Code in etwas anderes umschreibt.

  • Im C / C ++ - Fall gibt es keine grundlegende Überprüfung der Integrität. Wenn Sie vorsichtig sind, sind die Dinge in Ordnung. Wenn Sie einen Fehler machen oder Makros überbeanspruchen, können Sie große Probleme bekommen.

    Hinzu kommt, dass viele einfache Dinge, die Sie mit Makros im C / C ++ - Stil tun können, auf andere Weise in anderen Sprachen ausgeführt werden können.

  • In anderen Sprachen, wie z. B. verschiedenen Lisp-Dialekten, sind Makros besser in die Hauptsprachensyntax integriert, es können jedoch weiterhin Probleme mit Deklarationen in einem Makro auftreten, die "undicht" sind. Dies wird durch hygienische Makros behoben .


Kurzer historischer Kontext

Makros (Abkürzung für Macro-Instructions) tauchten erstmals im Kontext der Assemblersprache auf. Laut Wikipedia waren in den 1950er Jahren Makros in einigen IBM-Assemblern verfügbar.

Die ursprüngliche LISP hatte keine Makros, aber sie wurden erstmals Mitte der 1960er Jahre in MacLisp eingeführt: https://stackoverflow.com/questions/3065606/when-did-the-idea-of-macros-user-defined-code -Transformation-erscheinen . http://www.csee.umbc.edu/courses/331/resources/papers/Evolution-of-Lisp.pdf . Zuvor bot "fexprs" eine makrofähige Funktionalität.

Die frühesten Versionen von C hatten keine Makros ( http://cm.bell-labs.com/cm/cs/who/dmr/chist.html ). Diese wurden um 1972-73 über einen Präprozessor hinzugefügt. Vorher unterstützte C nur #includeund #define.

Der M4-Makro-Präprozessor stammt aus dem Jahr 1977.

Neuere Sprachen implementieren anscheinend Makros, bei denen das Operationsmodell syntaktisch und nicht textuell ist.

Wenn also jemand über den Vorrang einer bestimmten Definition des Begriffs "Makro" spricht, ist zu beachten, dass sich die Bedeutung im Laufe der Zeit weiterentwickelt hat.

Stephen C
quelle
9
C ++ ist mit ZWEI Makrosystemen gesegnet (?): Der Präprozessor- und Template-Erweiterung.
Jeffrey Hantin
Ich denke, dass die Behauptung, dass die Template-Erweiterung ein Makro ist, die Definition des Makros erweitert. Durch die Verwendung Ihrer Definition wird die ursprüngliche Frage bedeutungslos und einfach falsch, da die meisten modernen Sprachen tatsächlich Makros enthalten (entsprechend Ihrer Definition). Wenn die überwiegende Mehrheit der Entwickler jedoch Makros verwendet, ist dies eine gute Frage.
Dunk
@Dunk, die Definition eines Makros wie in Lisp geht dem "modernen" dummen Verständnis wie im pathetischen C-Präprozessor voraus.
SK-logic
@SK: Ist es besser, "technisch" korrekt zu sein und das Gespräch zu verschleiern, oder ist es besser zu verstehen?
Dunk
1
@Dunk, die Terminologie gehört nicht den unwissenden Horden. Ihre Ignoranz sollte niemals in Betracht gezogen werden, da sie sonst dazu führen wird, dass die gesamte Domäne der Informatik verdummt. Wenn jemand "Makro" nur als Verweis auf einen C-Präprozessor versteht, ist dies nichts anderes als Unwissenheit. Ich bezweifle, dass es in Office einen erheblichen Anteil von Menschen gibt, die noch nie von Lisp oder gar von VBA-Makros gehört haben.
SK-logic
12

Makros können, wie Scott bemerkt , das Ausblenden von Logik ermöglichen. Dies gilt natürlich auch für Funktionen, Klassen, Bibliotheken und viele andere gängige Geräte.

Ein leistungsfähiges Makrosystem kann jedoch noch weiter gehen und es Ihnen ermöglichen, Syntax und Strukturen zu entwerfen und zu verwenden, die in der Sprache normalerweise nicht zu finden sind. Dies kann in der Tat ein wunderbares Werkzeug sein: domänenspezifische Sprachen, Codegeneratoren und vieles mehr - alles in einer einzigen Sprachumgebung ...

Es kann jedoch missbraucht werden. Es kann das Lesen, Verstehen und Debuggen von Code erschweren, die Zeit verlängern, die neue Programmierer benötigen, um sich mit einer Codebasis vertraut zu machen, und zu kostspieligen Fehlern und Verzögerungen führen.

Für Sprachen, die die Programmierung vereinfachen sollen (wie Java oder Python), ist ein solches System ein Anathema.

Shog9
quelle
3
Und dann ging Java von den Schienen, indem es foreach, Anmerkungen, Behauptungen, Generics-by-Erasure hinzufügte, ...
Jeffrey Hantin
2
@ Jeffrey: und vergessen wir nicht, viele Codegeneratoren von Drittanbietern . Vergessen wir beim zweiten Gedanken diese.
Shog9
4
Ein weiteres Beispiel dafür, wie einfach Java war.
Jeffrey Hantin
3
@ JeffreyHantin: Was ist los mit Foreach?
Casebash
1
@Dunk Diese Analogie mag eine Strecke sein ... aber ich denke, die Erweiterbarkeit der Sprache ähnelt einer Art Plutonium. Kostspielig zu implementieren, sehr schwierig in die gewünschten Formen zu bearbeiten und bemerkenswert gefährlich, wenn sie falsch verwendet werden, aber auch erstaunlich leistungsfähig, wenn sie gut angewendet werden.
Jeffrey Hantin
6

Makros können unter bestimmten Umständen sehr sicher implementiert werden - in Lisp beispielsweise sind Makros nur Funktionen, die transformierten Code als Datenstruktur (s-Ausdruck) zurückgeben. Natürlich profitiert Lisp erheblich von der Tatsache, dass es homoikonisch ist und dass "Code Daten sind".

Ein Beispiel dafür, wie einfach Makros sein können, ist dieses Clojure-Beispiel, das einen Standardwert angibt, der im Falle einer Ausnahme verwendet werden soll:

(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

Aber auch in Lisps lautet der allgemeine Rat: "Verwenden Sie keine Makros, es sei denn, Sie müssen".

Wenn Sie keine homoikonische Sprache verwenden, werden Makros viel kniffliger, und die verschiedenen anderen Optionen weisen einige Probleme auf:

  • Textbasierte Makros - z. B. der C-Präprozessor - sind einfach zu implementieren, aber sehr schwierig zu verwenden, da Sie die richtige Quellensyntax in Textform generieren müssen, einschließlich aller syntaktischen Macken
  • Makrobasiertes DSLS - zB das C ++ Template System. Komplex, was selbst zu einer schwierigen Syntax führen kann, kann für Compiler- und Tool-Writer äußerst komplex sein, da die Sprachsyntax und -semantik erheblich komplexer wird.
  • AST / Bytecode-Manipulations-APIs - z. B. Java-Reflection / Bytecode-Generierung - sind theoretisch sehr flexibel, können jedoch sehr ausführlich sein: Es kann viel Code erforderlich sein, um ganz einfache Dinge zu tun. Wenn es zehn Codezeilen dauert, um das Äquivalent einer dreizeiligen Funktion zu generieren, haben Sie mit Ihren Meta-Programmierbemühungen nicht viel gewonnen ...

Darüber hinaus kann alles, was ein Makro kann, letztendlich auf eine andere Art und Weise in einer vollständigen Sprache erreicht werden (auch wenn dies das Schreiben einer großen Menge von Boilerplate bedeutet). Angesichts all dieser Schwierigkeiten ist es nicht verwunderlich, dass viele Sprachen der Meinung sind, dass die Implementierung von Makros nicht wirklich die Mühe wert ist.

mikera
quelle
Pfui. Nicht mehr "Code ist Daten ist eine gute Sache" Müll. Code ist Code, Daten sind Daten, und die nicht ordnungsgemäße Trennung der beiden ist eine Sicherheitslücke. Sie würden denken, dass das Aufkommen von SQL Injection als eine der größten existierenden Schwachstellenklassen die Idee, Code und Daten ein für alle Mal leicht austauschbar zu machen, in Abrede gestellt hätte.
Mason Wheeler
10
@Mason - Ich glaube nicht, dass Sie das Code-is-Data-Konzept verstehen. Jeglicher C-Programm-Quellcode ist ebenfalls Daten - er wird nur zufällig im Textformat ausgedrückt. Lisps sind die gleichen, außer dass sie den Code in praktischen Zwischendatenstrukturen (S-Ausdrücken) ausdrücken, die es ermöglichen, ihn vor dem Kompilieren durch Makros zu manipulieren und zu transformieren. In beiden Fällen könnte es eine Sicherheitslücke sein, nicht vertrauenswürdige Eingaben an den Compiler zu senden - aber das ist schwer aus Versehen zu tun und es wäre deine Schuld, wenn du etwas Dummes tust, nicht die des Compilers.
Mikera
Rust ist ein weiteres interessantes sicheres Makrosystem. Es verfügt über strenge Regeln und Semantiken, um die mit lexikalischen C / C ++ - Makros verbundenen Probleme zu vermeiden, und verwendet eine DSL, um Teile des Eingabeausdrucks in Makrovariablen zu erfassen, die später eingefügt werden. Es ist nicht so leistungsfähig wie die Fähigkeit von Lisps, eine Funktion auf dem Eingang aufzurufen, aber es bietet eine große Anzahl nützlicher Manipulationsmakros, die von anderen Makros aus aufgerufen werden können.
Zstewart
Pfui. Nicht mehr Sicherheitsmüll . Ansonsten sollten Sie jedes System mit integrierter von Neumann-Architektur beschuldigen, z. B. jeden IA-32-Prozessor mit der Fähigkeit, den Code selbst zu ändern. Ich vermute, dass Sie die Lücke physisch füllen können ... Wie auch immer, Sie müssen sich der Tatsache im Allgemeinen stellen: Isomorphie zwischen Code und Daten ist in mehrfacher Hinsicht Natur der Welt . Und (wahrscheinlich) haben Sie als Programmierer die Pflicht, die Unveränderlichkeit der Sicherheitsanforderungen aufrechtzuerhalten, was nicht dasselbe ist, wenn Sie vorzeitige Trennung künstlich überall anwenden.
FrankHB
4

Überlegen Sie sich zur Beantwortung Ihrer Fragen, wofür Makros überwiegend verwendet werden (Warnung: Gehirn-kompilierter Code).

  • Makros zum Definieren symbolischer Konstanten #define X 100

Dies kann leicht ersetzt werden durch: const int X = 100;

  • Makros, mit denen (im Wesentlichen) typunabhängige Inline-Funktionen definiert werden #define max(X,Y) (X>Y?X:Y)

In jeder Sprache, die das Überladen von Funktionen unterstützt, kann dies durch Überladen von Funktionen des richtigen Typs oder in einer Sprache, die Generika unterstützt, durch eine generische Funktion wesentlich typsicherer emuliert werden. Das Makro wird gerne versuchen, alles zu vergleichen , einschließlich Zeiger oder Zeichenfolgen, die möglicherweise kompiliert werden, aber mit ziemlicher Sicherheit nicht das sind, was Sie wollten. Auf der anderen Seite bieten Makros, die Sie typsicher gemacht haben, keine Vorteile oder Bequemlichkeiten gegenüber überladenen Funktionen.

  • Makros, mit denen Verknüpfungen zu häufig verwendeten Elementen angegeben werden. #define p printf

Dies kann leicht durch eine Funktion ersetzt werden p(), die dasselbe tut. Dies ist mit C verbunden (Sie müssen die va_arg()Funktionsfamilie verwenden), in vielen anderen Sprachen, die eine variable Anzahl von Funktionsargumenten unterstützen, ist dies jedoch viel einfacher.

Die Unterstützung dieser Funktionen in einer Sprache anstelle einer speziellen Makrosprache ist einfacher, weniger fehleranfällig und für andere, die den Code lesen, weitaus weniger verwirrend. Tatsächlich fällt mir kein einziger Anwendungsfall für Makros ein, der nicht einfach auf andere Weise dupliziert werden kann. Der einzige Ort, an dem Makros wirklich nützlich sind, ist, wenn sie an bedingte Kompilierungskonstrukte wie #if(usw.) gebunden sind .

In diesem Punkt werde ich nicht mit Ihnen streiten, da ich glaube, dass Nicht-Präprozessor-Lösungen für die bedingte Kompilierung in gängigen Sprachen äußerst umständlich sind (wie die Bytecode-Injektion in Java). Sprachen wie D haben jedoch Lösungen entwickelt, die keinen Präprozessor erfordern und nicht umständlicher sind als die Verwendung von Präprozessor-Bedingungen, während sie weitaus weniger fehleranfällig sind.

Chinmay Kanchi
quelle
1
Wenn Sie #max definieren müssen, setzen Sie bitte eckige Klammern um die Parameter, damit keine unerwarteten Auswirkungen der Operator-Rangfolge auftreten ... wie #max definieren (X, Y) ((X)> (Y)? (X) :( Y))
foo
Sie erkennen, dass es nur ein Beispiel war ... Die Absicht war es zu veranschaulichen.
Chinmay Kanchi
+1 für diesen systematischen Umgang mit der Materie. Ich möchte hinzufügen, dass die bedingte Kompilierung leicht zu einem Albtraum werden kann - ich erinnere mich, dass es ein Programm mit dem Namen "unifdef" (??) gab, dessen Zweck es war, sichtbar zu machen, was nach der Nachbearbeitung noch übrig war.
Ingo
7
In fact, I can't think of a single use-case for macros that can't easily be duplicated in another way: Zumindest in C können Sie mithilfe der Token-Verkettung keine Bezeichner (Variablennamen) bilden, ohne Makros zu verwenden.
Charles Salvia
Mit Makros kann sichergestellt werden, dass bestimmte Code- und Datenstrukturen "parallel" bleiben. Wenn beispielsweise eine kleine Anzahl von Bedingungen mit zugeordneten Nachrichten vorhanden ist und diese in einem übersichtlichen Format beibehalten werden müssen, kann der Versuch enum, die Bedingungen mithilfe von und ein konstantes Zeichenfolgenarray zum Definieren der Nachrichten zu definieren, zu Problemen führen, wenn enum und das Array nicht mehr synchron. Wenn Sie ein Makro verwenden, um alle Paare (Aufzählung, Zeichenfolge) zu definieren, und dieses Makro dann zweimal mit anderen geeigneten Definitionen im Gültigkeitsbereich einschließen, wird jeder Aufzählungswert neben die Zeichenfolge gesetzt.
Supercat
2

Das größte Problem, das ich bei Makros gesehen habe, ist, dass sie bei starker Verwendung das Lesen und Verwalten von Code sehr erschweren können, da Sie mit ihnen Logik im Makro verbergen können, die möglicherweise leicht zu finden ist (und möglicherweise trivial ist oder auch nicht ).

Scott Dorman
quelle
Sollte das Makro nicht dokumentiert werden?
Casebash
@Casebash: Natürlich sollte das Makro wie jeder andere Quellcode dokumentiert sein ... aber in der Praxis habe ich es selten gesehen.
Scott Dorman
3
Debuggen Sie jemals Dokumentation? Es ist einfach nicht genug, wenn schlecht geschriebener Code verwendet wird.
JeffO
OOP hat auch einige dieser Probleme ...
aoeu256
2

Beginnen Sie mit der Feststellung, dass MAKROs in C / C ++ sehr begrenzt, fehleranfällig und nicht wirklich nützlich sind.

MAKROs, wie sie beispielsweise in LISP oder z / OS-Assemblersprache implementiert sind, können zuverlässig und unglaublich nützlich sein.

Aber wegen des Missbrauchs der eingeschränkten Funktionalität in C haben sie sich einen schlechten Ruf erworben. Daher implementiert niemand mehr Makros. Stattdessen erhalten Sie Templates, die einige der einfachen Stuff-Makros ausführen, und Java-Annotationen, die einige der komplexeren Stuff-Makros ausführen.

James Anderson
quelle