Warum sind Präprozessor-Makros böse und welche Alternativen gibt es?


91

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?


18
#pragmaist kein Makro.
FooF

@foof Präprozessor-Direktive?
user1849534

6
@ user1849534: Ja, das ist es ... und Ratschläge zu Makros sprechen nicht darüber #pragma.
Ben Voigt

1
Sie können viel mit constexpr, inlineFunktionen und templates, aber tun boost.preprocessorund chaoszeigen, dass Makros ihren Platz haben. Ganz zu schweigen von Konfigurationsmakros für verschiedene Compiler, Plattformen usw.
Brandon

Antworten:


160

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

  1. Sie können keine Makros debuggen.
  2. Makroerweiterung kann zu seltsamen Nebenwirkungen führen.
  3. Makros haben keinen "Namespace". Wenn Sie also ein Makro haben, das mit einem an anderer Stelle verwendeten Namen kollidiert, erhalten Sie Makroersetzungen, wo Sie es nicht wollten, und dies führt normalerweise zu seltsamen Fehlermeldungen.
  4. Makros können sich auf Dinge auswirken, die Sie nicht erkennen.

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 enumoderconst 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 Verwendung x2 = SQUARE(x++). Das führt dazu x2 = (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:

#define safe_divide(res, x, y)   if (y != 0) res = x/y;

und dann

if (something) safe_divide(b, a, x);
else printf("Something is not set...");

Es wird tatsächlich völlig falsch ....

Ersatz : echte Funktionen.

3) Makros haben keinen Namespace

Wenn wir ein Makro haben:

#define begin() x = 0

und wir haben einen Code in C ++, der begin verwendet:

std::vector<int> v;

... stuff is loaded into v ... 

for (std::vector<int>::iterator it = myvector.begin() ; it != myvector.end(); ++it)
   std::cout << ' ' << *it;

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:

#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();

}

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:

#define malloc(x) my_debug_malloc(x, __FILE__, __LINE__)
#define free(x)  my_debug_free(x, __FILE__, __LINE__)

Jetzt können wir my_debug_mallocals 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 also xzweimal aktualisiert , was undefiniert ist und wahrscheinlich zu unterschiedlichen Werten auf verschiedenen Systemen und auch zu unterschiedlichen Ergebniswerten führen wird x.


6
Die if elseProbleme können gelöst werden, indem der Makrokörper darin eingewickelt wird do { ... } while(0). Diese verhält sich als würde man in Bezug auf erwarten ifund forund 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)
Aaron McDaid

11
@AaronMcDaid: Ja, es gibt einige Problemumgehungen, die einige der Probleme lösen, die in diesen Makros auftreten. Der springende Punkt meines Beitrags war nicht zu zeigen, wie man Makros gut macht, sondern "wie einfach es ist, Makros falsch zu machen", wo es eine gute Alternative gibt. Es gibt jedoch Dinge, die Makros sehr leicht lösen können, und es gibt Zeiten, in denen Makros auch das Richtige sind.
Mats Petersson

1
In Punkt 3 sind die Fehler kein Problem mehr. Moderne Compiler wie Clang werden so etwas sagen note: expanded from macro 'begin'und zeigen, wo begines definiert ist.
kirbyfan64sos

5
Makros sind schwer in andere Sprachen zu übersetzen.
Marco van de Voort

1
@FrancescoDondi: stackoverflow.com/questions/4176328/… (ziemlich weit unten in dieser Antwort, es geht um i ++ * i ++ und so.
Mats Petersson

21

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

Mit dem neuen C ++ 11 gibt es nach so vielen Jahren eine echte Alternative?

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:

#define max(a, b) ( ((a) > (b)) ? (a) : (b) )

int a = 5;
int b = 4;

int c = max(++a, b);

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:

int c = ( ((++a) ? (b)) ? (++a) : (b) ); // after this, c = a = 7

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:

#ifdef max
#undef max
#endif
#include <algorithm>

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 maxoben genannten:

template<typename T>
inline T max(const T& a, const T& b)
{
    return a > b ? a : b;
}

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

int a = 0;
double b = 1.;
max(a, b);

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 sagen max<double>(a, b)(und daher Ihre Absicht explizit angeben ).


1
Es muss nicht c ++ 11-spezifisch sein. Sie können einfach Funktionen verwenden, um die Verwendung von Makros als Ausdrücke zu ersetzen, und [statisch] const / constexpr, um die Verwendung von Makros als Konstanten zu ersetzen.
utnapistim

1
Sogar C99 erlaubt die Verwendung von 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.
Mats Petersson

Ein Pre-Inkrement durchzuführen, während ein Argument übergeben wird, ist eine schreckliche Codierungspraxis. Und jeder, der in C / C ++ codiert, sollte nicht davon ausgehen, dass ein funktionsähnlicher Aufruf kein Makro ist.
StephenG

Viele Implementierungen setzen die Bezeichner freiwillig in Klammern maxund minwenn ihnen eine linke Klammer folgt. Aber Sie sollten solche Makros nicht definieren ...
LF

14

Ein häufiges Problem ist Folgendes:

#define DIV(a,b) a / b

printf("25 / (3+2) = %d", DIV(25,3+2));

Es wird 10 gedruckt, nicht 5, da der Präprozessor es folgendermaßen erweitert:

printf("25 / (3+2) = %d", 25 / 3 + 2);

Diese Version ist sicherer:

#define DIV(a,b) (a) / (b)

2
interessantes Beispiel, im Grunde sind sie nur Token ohne Semantik
user1849534

Ja. Sie werden so erweitert, wie sie dem Makro zugewiesen werden. Das DIVMakro kann mit einem Paar () umgeschrieben werden b.
Phaazon

2
Du meinst #define DIV(a,b)nicht #define DIV (a,b), was sehr unterschiedlich ist.
Rici

6
#define DIV(a,b) (a) / (b)ist nicht gut genug; #define DIV(a,b) ( (a) / (b) )
Fügen Sie in

3

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.

#define MULTIPLY(a,b) a*b

und dann anrufen

MULTIPLY(2+3,4+5)

das erweitert sich in

2 + 3 * 4 + 5 (und nicht in: (2 + 3) * (4 + 5)).

Um letzteres zu haben, sollten Sie definieren:

#define MULTIPLY(a,b) ((a)*(b))

3

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.


Wie aus anderen Antworten hervorgeht, können schlecht gestaltete Präprozessordefinitionen Code mit gültiger Syntax, aber unterschiedlicher semantischer Bedeutung erzeugen. Dies bedeutet, dass sich der Compiler nicht beschwert und Sie einen Fehler in Ihrem Code eingeführt haben, der noch schwerer zu finden sein wird.
Sandi Hrvić

3

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

#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

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.


Das Festlegen von Makrowerten in der cmdline während der Kompilierung, um zwei Varianten aus einer Codebasis zu erstellen, ist wirklich hilfreich. in Maßen.
Kevinf

1
Aus einer bestimmten Perspektive ist diese Verwendung die gefährlichste: Tools (IDEs, statische Analysatoren, Refactoring) werden es schwer haben, die möglichen Codepfade herauszufinden.
Erenon

2

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.


2
Was lässt Sie glauben, dass Makros nicht gut optimiert sind? Es handelt sich um eine einfache Textersetzung, und das Ergebnis wird genauso optimiert wie Code, der ohne Makros geschrieben wurde.
Ben Voigt

@BenVoigt, aber sie berücksichtigen die Semantik nicht und dies kann zu etwas führen, das als "nicht optimal" angesehen werden kann ... zumindest ist dies mein erster Hinweis auf diesen stackoverflow.com/a/14041502/1849534
user1849534

@ user1849534: Das bedeutet das Wort "optimiert" im Kontext der Kompilierung nicht.
Ben Voigt

1
@ BenVoigt Genau, Makros sind nur Textersetzung. Der Compiler dupliziert nur den Code. Dies ist kein Leistungsproblem, kann jedoch die Programmgröße erhöhen. Dies gilt insbesondere in einigen Kontexten, in denen die Programmgröße eingeschränkt ist. Einige Codes sind so voll mit Makros, dass die Größe des Programms doppelt so hoch ist.
Davide Icardi

1

Präprozessor-Makros sind nicht böse, wenn sie für beabsichtigte Zwecke verwendet werden, wie:

  • Erstellen verschiedener Versionen derselben Software mit Konstrukten vom Typ #ifdef, z. B. Freigeben von Fenstern für verschiedene Regionen.
  • Zum Definieren von Werten für Codetests.

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.


1
seit C ++ 17 constexpr if + kann eine Header-Datei, die "config" constexpr-Variablen enthält, die # ifdefs ersetzen.
Enhex
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.