Ich habe das immer gefragt, aber ich habe nie eine wirklich gute Antwort erhalten; Ich denke, dass fast jeder Programmierer vor dem Schreiben der ersten "Hallo Welt" auf einen Satz wie "Makro sollte niemals verwendet werden", "Makro ist böse" usw. gestoßen ist. Meine Frage lautet: Warum? Gibt es mit dem neuen C ++ 11 nach so vielen Jahren eine echte Alternative?
Der einfache Teil handelt von Makros wie #pragma
, die plattformspezifisch und compilerspezifisch sind, und meistens weisen sie schwerwiegende Fehler auf #pragma once
, die in mindestens zwei wichtigen Situationen fehleranfällig sind: Gleicher Name in verschiedenen Pfaden und bei einigen Netzwerkeinstellungen und Dateisystemen.
Aber was ist im Allgemeinen mit Makros und Alternativen zu ihrer Verwendung?
quelle
#pragma
ist kein Makro.#pragma
.constexpr
,inline
Funktionen undtemplates
, aber tunboost.preprocessor
undchaos
zeigen, dass Makros ihren Platz haben. Ganz zu schweigen von Konfigurationsmakros für verschiedene Compiler, Plattformen usw.Antworten:
Makros sind wie jedes andere Werkzeug - ein Hammer, der bei einem Mord verwendet wird, ist nicht böse, weil es ein Hammer ist. Es ist böse in der Art, wie die Person es auf diese Weise benutzt. Wenn Sie Nägel einschlagen möchten, ist ein Hammer das perfekte Werkzeug.
Makros haben einige Aspekte, die sie "schlecht" machen (ich werde sie später näher erläutern und Alternativen vorschlagen):
Lassen Sie uns hier ein wenig erweitern:
1) Makros können nicht debuggt werden. Wenn Sie ein Makro haben, das in eine Zahl oder eine Zeichenfolge übersetzt wird, hat der Quellcode den Makronamen und viele Debugger können nicht "sehen", in was das Makro übersetzt wird. Sie wissen also nicht genau, was los ist.
Ersatz : Verwenden Sie
enum
oderconst T
Bei "funktionsähnlichen" Makros verhält sich Ihr Makro wie eine einzelne Anweisung, unabhängig davon, ob es sich um eine oder hundert Anweisungen handelt, da der Debugger auf der Ebene "pro Quellzeile, auf der Sie sich befinden" arbeitet. Macht es schwer herauszufinden, was los ist.
Ersatz : Verwenden Sie Funktionen - Inline, wenn es "schnell" sein muss (aber beachten Sie, dass zu viel Inline keine gute Sache ist)
2) Makroerweiterungen können seltsame Nebenwirkungen haben.
Der berühmte ist
#define SQUARE(x) ((x) * (x))
und die Verwendungx2 = SQUARE(x++)
. Das führt dazux2 = (x++) * (x++);
, dass, selbst wenn es ein gültiger Code wäre [1], mit ziemlicher Sicherheit nicht das wäre, was der Programmierer wollte. Wenn es eine Funktion wäre, wäre es in Ordnung, x ++ auszuführen, und x würde nur einmal inkrementieren.Ein anderes Beispiel ist "wenn sonst" in Makros. Sagen wir, wir haben folgendes:
und dann
Es wird tatsächlich völlig falsch ....
Ersatz : echte Funktionen.
3) Makros haben keinen Namespace
Wenn wir ein Makro haben:
und wir haben einen Code in C ++, der begin verwendet:
Welche Fehlermeldung erhalten Sie Ihrer Meinung nach und wo suchen Sie nach einem Fehler [vorausgesetzt, Sie haben das Startmakro, das sich in einer Header-Datei befindet, die jemand anderes geschrieben hat, vollständig vergessen oder gar nicht gekannt? [und noch mehr Spaß, wenn Sie dieses Makro vor dem Einschließen einfügen - Sie würden in seltsamen Fehlern ertrinken, die absolut keinen Sinn ergeben, wenn Sie sich den Code selbst ansehen.
Ersetzung : Nun, es gibt nicht einmal eine Ersetzung als eine "Regel" - verwenden Sie nur Großbuchstaben für Makros und niemals alle Großbuchstaben für andere Dinge.
4) Makros haben Effekte, die Sie nicht erkennen
Nehmen Sie diese Funktion:
Ohne das Makro zu betrachten, würden Sie denken, dass begin eine Funktion ist, die x nicht beeinflussen sollte.
Diese Art von Dingen, und ich habe viel komplexere Beispiele gesehen, kann Ihren Tag WIRKLICH durcheinander bringen!
Ersetzung : Verwenden Sie entweder kein Makro, um x festzulegen, oder übergeben Sie x als Argument.
Es gibt Zeiten, in denen die Verwendung von Makros definitiv von Vorteil ist. Ein Beispiel ist das Umschließen einer Funktion mit Makros, um Datei- / Zeileninformationen weiterzugeben:
Jetzt können wir
my_debug_malloc
als reguläres Malloc im Code verwenden, aber es hat zusätzliche Argumente. Wenn es also zum Ende kommt und wir scannen, "welche Speicherelemente nicht freigegeben wurden", können wir drucken, wo die Zuordnung vorgenommen wurde Der Programmierer kann das Leck aufspüren.[1] Es ist ein undefiniertes Verhalten, eine Variable mehr als einmal "in einem Sequenzpunkt" zu aktualisieren. Ein Sequenzpunkt ist nicht genau dasselbe wie eine Aussage, aber für die meisten Absichten und Zwecke sollten wir ihn so betrachten. Das
x++ * x++
wird alsox
zweimal aktualisiert , was undefiniert ist und wahrscheinlich zu unterschiedlichen Werten auf verschiedenen Systemen und auch zu unterschiedlichen Ergebniswerten führen wirdx
.quelle
if else
Probleme können gelöst werden, indem der Makrokörper darin eingewickelt wirddo { ... } while(0)
. Diese verhält sich als würde man in Bezug auf erwartenif
undfor
und anderen potenziell riskante Steuer Flußprobleme. Aber ja, eine echte Funktion ist normalerweise eine bessere Lösung.#define macro(arg1) do { int x = func(arg1); func2(x0); } while(0)
note: expanded from macro 'begin'
und zeigen, wobegin
es definiert ist.Das Sprichwort "Makros sind böse" bezieht sich normalerweise auf die Verwendung von #define, nicht #pragma.
Insbesondere bezieht sich der Ausdruck auf diese beiden Fälle:
magische Zahlen als Makros definieren
Verwenden von Makros zum Ersetzen von Ausdrücken
Ja, für die Elemente in der obigen Liste (magische Zahlen sollten mit const / constexpr definiert werden und Ausdrücke sollten mit den Funktionen [normal / inline / template / inline template] definiert werden.
Hier sind einige der Probleme, die durch das Definieren magischer Zahlen als Makros und das Ersetzen von Ausdrücken durch Makros entstehen (anstatt Funktionen zum Auswerten dieser Ausdrücke zu definieren):
Beim Definieren von Makros für magische Zahlen behält der Compiler keine Typinformationen für die definierten Werte bei. Dies kann zu Kompilierungswarnungen (und Fehlern) führen und die Benutzer beim Debuggen des Codes verwirren.
Wenn Programmierer Makros anstelle von Funktionen definieren, erwarten sie, dass sie wie Funktionen funktionieren, und dies nicht.
Betrachten Sie diesen Code:
Sie würden erwarten, dass a und c nach der Zuweisung zu c 6 sind (wie bei Verwendung von std :: max anstelle des Makros). Stattdessen führt der Code Folgendes aus:
Darüber hinaus unterstützen Makros keine Namespaces. Das bedeutet, dass das Definieren von Makros in Ihrem Code den Clientcode in den Namen einschränkt, die sie verwenden können.
Dies bedeutet, dass Sie, wenn Sie das Makro oben (für max.) Definieren,
#include <algorithm>
in keinem der folgenden Codes mehr können , es sei denn, Sie schreiben explizit:Makros anstelle von Variablen / Funktionen bedeuten auch, dass Sie deren Adresse nicht übernehmen können:
Wenn ein Makro als Konstante eine magische Zahl ergibt, können Sie es nicht als Adresse übergeben
Für ein Makro als Funktion können Sie es nicht als Prädikat verwenden, die Adresse der Funktion nicht übernehmen oder als Funktor behandeln.
Bearbeiten: Als Beispiel die richtige Alternative zu den
#define max
oben genannten:Dies macht alles, was das Makro tut, mit einer Einschränkung: Wenn die Arten der Argumente unterschiedlich sind, zwingt Sie die Vorlagenversion dazu, explizit zu sein (was tatsächlich zu sichererem, expliziterem Code führt):
Wenn dieses Maximum als Makro definiert ist, wird der Code kompiliert (mit einer Warnung).
Wenn dieses Maximum als Vorlagenfunktion definiert ist, weist der Compiler auf die Mehrdeutigkeit hin, und Sie müssen entweder
max<int>(a, b)
oder sagenmax<double>(a, b)
(und daher Ihre Absicht explizit angeben ).quelle
const int someconstant = 437;
und es kann fast auf jede Art und Weise verwendet werden, wie ein Makro verwendet werden würde. Ebenso für kleine Funktionen. Es gibt einige Dinge, bei denen Sie etwas als Makro schreiben können, das in einem regulären Ausdruck in C nicht funktioniert (Sie könnten etwas erstellen, das ein Array eines beliebigen Zahlentyps mittelt, was C nicht kann - aber C ++ verfügt über Vorlagen dafür). Während C ++ 11 einige weitere Dinge hinzufügt, die "Sie brauchen keine Makros dafür", ist es meistens bereits in früheren C / C ++ gelöst.max
undmin
wenn ihnen eine linke Klammer folgt. Aber Sie sollten solche Makros nicht definieren ...Ein häufiges Problem ist Folgendes:
Es wird 10 gedruckt, nicht 5, da der Präprozessor es folgendermaßen erweitert:
Diese Version ist sicherer:
quelle
DIV
Makro kann mit einem Paar () umgeschrieben werdenb
.#define DIV(a,b)
nicht#define DIV (a,b)
, was sehr unterschiedlich ist.#define DIV(a,b) (a) / (b)
ist nicht gut genug;#define DIV(a,b) ( (a) / (b) )
Makros sind besonders nützlich, um generischen Code zu erstellen (Makroparameter können beliebig sein), manchmal mit Parametern.
Außerdem wird dieser Code an der Stelle platziert, an der das Makro verwendet wird.
OTOH, ähnliche Ergebnisse können erzielt werden mit:
überladene Funktionen (verschiedene Parametertypen)
Vorlagen in C ++ (generische Parametertypen und -werte)
Inline-Funktionen (Platzieren Sie den Code dort, wo sie aufgerufen werden, anstatt zu einer Einzelpunktdefinition zu springen - dies ist jedoch eher eine Empfehlung für den Compiler).
edit: warum die Makros schlecht sind:
1) Keine Typprüfung der Argumente (sie haben keinen Typ), kann also leicht missbraucht werden. 2) Erweitern Sie sich manchmal zu sehr komplexem Code, der in der vorverarbeiteten Datei schwer zu identifizieren und zu verstehen ist. 3) Es ist leicht, Fehler zu machen -anfälliger Code in Makros, wie z.
und dann anrufen
das erweitert sich in
2 + 3 * 4 + 5 (und nicht in: (2 + 3) * (4 + 5)).
Um letzteres zu haben, sollten Sie definieren:
quelle
Ich glaube nicht, dass die Verwendung von Präprozessordefinitionen oder Makros, wie Sie sie nennen, falsch ist.
Sie sind ein (Meta-) Sprachkonzept, das in c / c ++ zu finden ist, und wie jedes andere Tool können sie Ihnen das Leben erleichtern, wenn Sie wissen, was Sie tun. Das Problem mit Makros ist, dass sie vor Ihrem c / c ++ - Code verarbeitet werden und neuen Code generieren, der fehlerhaft sein und Compilerfehler verursachen kann, die alles andere als offensichtlich sind. Auf der positiven Seite können sie Ihnen helfen, Ihren Code sauber zu halten und Ihnen bei richtiger Verwendung viel Tipparbeit zu ersparen, sodass es auf Ihre persönlichen Vorlieben ankommt.
quelle
Makros in C / C ++ können als wichtiges Werkzeug für die Versionskontrolle dienen. Der gleiche Code kann mit einer geringfügigen Konfiguration von Makros an zwei Clients gesendet werden. Ich benutze Dinge wie
Diese Art von Funktionalität ist ohne Makros nicht so einfach möglich. Makros sind ein großartiges Tool zur Verwaltung der Softwarekonfiguration und nicht nur eine Möglichkeit, Verknüpfungen für die Wiederverwendung von Code zu erstellen. Das Definieren von Funktionen zum Zwecke der Wiederverwendbarkeit in Makros kann definitiv zu Problemen führen.
quelle
Ich denke, das Problem ist, dass Makros vom Compiler nicht gut optimiert werden und "hässlich" zu lesen und zu debuggen sind.
Oft sind generische Funktionen und / oder Inline-Funktionen eine gute Alternative.
quelle
Präprozessor-Makros sind nicht böse, wenn sie für beabsichtigte Zwecke verwendet werden, wie:
Alternativen - Für ähnliche Zwecke können Konfigurationsdateien im Ini-, XML- und JSON-Format verwendet werden. Ihre Verwendung hat jedoch Laufzeiteffekte auf Code, die ein Präprozessor-Makro vermeiden kann.
quelle