Warum sind Präprozessor-Makros böse und was sind die Alternativen?

Lesezeit: 13 Minuten

Warum sind Praprozessor Makros bose und was sind die Alternativen
Benutzer1849534

Ich habe das immer gefragt, aber ich habe nie eine wirklich gute Antwort bekommen; Ich denke, dass fast jeder Programmierer schon vor dem Schreiben des ersten „Hello World“ auf einen Ausdruck wie „Makros sollten niemals verwendet werden“, „Makros sind böse“ und so weiter gestoßen ist. Meine Frage ist: Warum? Gibt es mit dem neuen C++11 nach so vielen Jahren eine echte Alternative?

Der einfache Teil dreht sich um Makros wie #pragmadie plattformspezifisch und Compiler-spezifisch sind, und meistens haben sie schwerwiegende Fehler wie #pragma once das ist in mindestens 2 wichtigen Situationen fehleranfällig: gleicher Name in verschiedenen Pfaden und mit einigen Netzwerk-Setups und Dateisystemen.

Aber was ist im Allgemeinen mit Makros und Alternativen zu ihrer Verwendung?

  • #pragma ist kein Makro.

    – FooF

    26. Dezember 2012 um 13:47 Uhr

  • @foof Präprozessordirektive ?

    – Benutzer1849534

    26. Dezember 2012 um 13:51 Uhr

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

    – Ben Voigt

    26. Dezember 2012 um 13:52 Uhr

  • Du kannst viel damit machen constexpr, inline Funktionen und templatesaber boost.preprocessor und chaos zeigen, dass Makros ihre Berechtigung haben. Ganz zu schweigen von Konfigurationsmakros für verschiedene Compiler, Plattformen usw.

    – Brandon

    26. Dezember 2012 um 13:52 Uhr

  • Siehe auch “Sind alle Makros böse?”

    – Brad Larson

    23. Januar 2013 um 18:56 Uhr

Warum sind Praprozessor Makros bose und was sind die Alternativen
Matt Petersson

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, wie die Person es auf diese Weise benutzt. Wenn Sie Nägel einschlagen möchten, ist ein Hammer ein perfektes Werkzeug.

Es gibt ein paar Aspekte bei Makros, die sie “schlecht” machen (ich werde später darauf eingehen und Alternativen vorschlagen):

  1. Makros können nicht debuggt werden.
  2. Die Makroerweiterung kann zu seltsamen Nebeneffekten führen.
  3. Makros haben keinen “Namespace”, wenn Sie also ein Makro haben, das mit einem anderswo verwendeten Namen kollidiert, erhalten Sie Makroersetzungen, wo Sie es nicht wollten, und dies führt normalerweise zu seltsamen Fehlermeldungen.
  4. Makros können Dinge beeinflussen, von denen Sie nichts wissen.

Lassen Sie uns hier also etwas erweitern:

1) Makros können nicht debuggt werden.
Wenn Sie ein Makro haben, das in eine Zahl oder einen String übersetzt wird, enthält der Quellcode den Makronamen, und viele Debugger können nicht „sehen“, was das Makro übersetzt. Sie wissen also nicht wirklich, was los ist.

Ersatz: Verwenden enum oder const T

Da der Debugger bei “funktionsähnlichen” Makros auf einer Ebene “pro Quellzeile, wo Sie sich befinden” arbeitet, verhält sich Ihr Makro wie eine einzelne Anweisung, egal ob es sich um eine oder hundert Anweisungen handelt. Macht es schwer herauszufinden, was los ist.

Ersatz: Verwenden Sie Funktionen – Inline, wenn es “schnell” sein muss (aber Vorsicht, zu viel Inline ist nicht gut)

2) Makroerweiterungen können seltsame Nebeneffekte haben.

Der berühmte ist #define SQUARE(x) ((x) * (x)) und die Verwendung x2 = SQUARE(x++). Das führt zu x2 = (x++) * (x++);was, auch wenn es sich um einen gültigen Code handelte [1], wäre mit ziemlicher Sicherheit nicht das, 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 weiteres Beispiel ist “if else” in Makros, sagen wir, wir haben dies:

#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 eigentlich völlig falsch….

Ersatz: reelle Funktionen.

3) Makros haben keinen Namensraum

Wenn wir ein Makro haben:

#define begin() x = 0

und wir haben 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;

Nun, welche Fehlermeldung erhalten Sie Ihrer Meinung nach und wo suchen Sie nach einem Fehler? [assuming you have completely forgotten – or didn’t even know about – the begin macro that lives in some header file that someone else wrote? [and even more fun if you included that macro before the include – you’d be drowning in strange errors that makes absolutely no sense when you look at the code itself.

Replacement: Well there isn’t so much as a replacement as a “rule” – only use uppercase names for macros, and never use all uppercase names for other things.

4) Macros have effects you don’t realize

Take this function:

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

}

Now, without looking at the macro, you would think that begin is a function, which shouldn’t affect x.

This sort of thing, and I’ve seen much more complex examples, can REALLY mess up your day!

Replacement: Either don’t use a macro to set x, or pass x in as an argument.

There are times when using macros is definitely beneficial. One example is to wrap a function with macros to pass on file/line information:

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

Now we can use my_debug_malloc as the regular malloc in the code, but it has extra arguments, so when it comes to the end and we scan the “which memory elements hasn’t been freed”, we can print where the allocation was made so the programmer can track down the leak.

[1] Es ist ein undefiniertes Verhalten, eine Variable mehr als einmal “an einem Sequenzpunkt” zu aktualisieren. Ein Sequenzpunkt ist nicht genau dasselbe wie eine Anweisung, aber für die meisten Absichten und Zwecke sollten wir es so betrachten. Also tun x++ * x++ werde Dich auf dem Laufenden halten x zweimal, was undefiniert ist und wahrscheinlich zu unterschiedlichen Werten auf verschiedenen Systemen und zu unterschiedlichen Ergebniswerten führen wird x auch.

  • Die if else Probleme können gelöst werden, indem der Makrokörper nach innen gewickelt wird do { ... } while(0). Diese verhält sich bzgl. wie man es erwarten würde if und for und andere potenziell riskante Kontrollflussprobleme. Aber ja, eine echte Funktion ist meistens die bessere Lösung. #define macro(arg1) do { int x = func(arg1); func2(x0); } while(0)

    – Aaron McDaid

    10. Oktober 2013 um 22:10 Uhr


  • @AaronMcDaid: Ja, es gibt einige Problemumgehungen, die einige der Probleme lösen, die in diesen Makros auftauchen. Der springende Punkt in meinem Beitrag war nicht, zu zeigen, wie man Makros gut macht, sondern „wie leicht es ist, Makros falsch zu machen“, wo es eine gute Alternative gibt. Allerdings gibt es Dinge, die Makros sehr einfach lösen, und es gibt Zeiten, in denen Makros auch das Richtige sind.

    – Mats Petersson

    11. Oktober 2013 um 6:10 Uhr

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

    – kirbyfan64sos

    5. März 2015 um 20:50 Uhr

  • Makros sind schwer in andere Sprachen zu übersetzen.

    – Marco van de Voort

    1. März 2016 um 8:29 Uhr

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

    – Mats Petersson

    9. September 2017 um 6:03 Uhr

1647193215 436 Warum sind Praprozessor Makros bose und was sind die Alternativen
utnapistim

Das Sprichwort „Makros sind böse“ bezieht sich normalerweise auf die Verwendung von #define, nicht auf #pragma.

Konkret bezieht sich der Ausdruck auf diese beiden Fälle:

  • magische Zahlen als Makros definieren

  • Verwendung 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 definiert werden [normal/inline/template/inline template] Funktionen.

Hier sind einige der Probleme, die durch die Definition 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. Dies kann zu Kompilierungswarnungen (und Fehlern) führen und Leute verwirren, die den Code debuggen.

  • Beim Definieren von Makros anstelle von Funktionen erwarten Programmierer, die diesen Code verwenden, dass sie wie Funktionen funktionieren, und das tun sie 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 es bei Verwendung von std::max anstelle des Makros der Fall wäre). 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, was bedeutet, dass das Definieren von Makros in Ihrem Code den Clientcode auf die Namen beschränkt, die sie verwenden können.

Das bedeutet, wenn Sie das Makro oben (für max) definieren, können Sie dies nicht mehr tun #include <algorithm> in einem der folgenden Codes, es sei denn, Sie schreiben ausdrücklich:

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

Makros anstelle von Variablen / Funktionen zu haben bedeutet auch, dass Sie ihre Adresse nicht nehmen können:

  • Wenn ein Makro als Konstante zu einer magischen Zahl ausgewertet wird, können Sie es nicht per Adresse übergeben

  • Für ein Makro als Funktion können Sie es nicht als Prädikat verwenden oder die Adresse der Funktion nehmen oder sie als Funktor behandeln.

Edit: Als Beispiel die richtige Alternative zum #define max Oben:

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 Typen der Argumente unterschiedlich sind, zwingt Sie die Vorlagenversion, 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 beides sagen max<int>(a, b) oder max<double>(a, b) (und geben Sie damit ausdrücklich Ihre Absicht an).

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

    – utnapistim

    26. Dezember 2012 um 14:23 Uhr

  • Auch C99 erlaubt die Verwendung von const int someconstant = 437;, und es kann fast auf jede Weise verwendet werden, auf die ein Makro verwendet würde. Ebenso für kleine Funktionen. Es gibt ein paar Dinge, bei denen Sie etwas als Makro schreiben können, das in einem regulären Ausdruck in C nicht funktioniert (Sie könnten etwas machen, das ein Array aus jeder Art von Zahl mittelt, was C nicht kann – aber C++ hat Vorlagen dafür). Während C++11 ein paar weitere Dinge hinzufügt, „dafür braucht man keine Makros“, ist es größtenteils bereits in früheren C/C++ gelöst.

    – Mats Petersson

    26. Dezember 2012 um 14:39 Uhr

  • Ein Pre-Inkrement durchzuführen, während ein Argument übergeben wird, ist eine schreckliche Programmierpraxis. Und jeder, der in C/C++ programmiert, sollte es tun nicht Angenommen, ein funktionsähnlicher Aufruf ist kein Makro.

    – StephenG – Helfen Sie der Ukraine

    14. Mai 2018 um 9:29 Uhr

  • Viele Implementierungen setzen die Bezeichner freiwillig in Klammern max und min wenn ihnen eine linke Klammer folgt. Aber man sollte solche Makros nicht definieren …

    – LF

    12. Juli 2019 um 5:24 Uhr

1647193215 655 Warum sind Praprozessor Makros bose und was sind die Alternativen
Phaazon

Ein häufiges Problem ist folgendes:

#define DIV(a,b) a / b

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

Es wird 10 ausgegeben, nicht 5, weil der Präprozessor es auf diese Weise erweitert:

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

Diese Version ist sicherer:

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

  • interessantes Beispiel, im Grunde sind sie nur Token ohne Semantik

    – Benutzer1849534

    26. Dezember 2012 um 13:53 Uhr

  • Jawohl. Sie werden so erweitert, wie sie dem Makro übergeben werden. Die DIV Makro kann mit einem Paar () umgeschrieben werden b.

    – Phaazon

    26. Dezember 2012 um 13:56 Uhr

  • Was meinen Sie #define DIV(a,b)nicht #define DIV (a,b)was sehr unterschiedlich ist.

    – Rici

    26. Dezember 2012 um 15:43 Uhr

  • #define DIV(a,b) (a) / (b) ist nicht gut genug; Fügen Sie als allgemeine Praxis immer die äußersten Klammern hinzu, wie folgt: #define DIV(a,b) ( (a) / (b) )

    – PJTrail

    13. Juni 2016 um 21:36 Uhr

Makros sind besonders wertvoll, um generischen Code zu erstellen (die Parameter von Makros können alles sein), manchmal mit Parametern.

Außerdem wird dieser Code an der Stelle platziert (dh eingefügt), 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 das Makro schlecht ist:

1) keine Typprüfung der Argumente (sie haben keinen Typ), können also leicht missbraucht werden 2) erweitern 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 zum Beispiel:

#define MULTIPLY(a,b) a*b

und dann anrufen

MULTIPLY(2+3,4+5)

das sich ausdehnt

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

Um letzteres zu haben, sollten Sie Folgendes definieren:

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

Ich glaube nicht, dass etwas falsch daran ist, Präprozessordefinitionen oder Makros zu verwenden, wie Sie sie nennen.

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 Compiler-Fehler 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, also kommt es auf Ihre persönlichen Vorlieben an.

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

    – Sandi Hrvic

    26. Dezember 2012 um 14:04 Uhr

1647193216 612 Warum sind Praprozessor Makros bose und was sind die Alternativen
indiangarg

Makros in C/C++ können als wichtiges Werkzeug zur Versionskontrolle dienen. Derselbe Code kann mit einer geringfügigen Konfiguration von Makros an zwei Clients geliefert 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 ohne weiteres möglich. Makros sind eigentlich ein großartiges Softwarekonfigurations-Management-Tool und nicht nur eine Möglichkeit, Verknüpfungen für die Wiederverwendung von Code zu erstellen. Das Definieren von Funktionen zwecks Wiederverwendbarkeit in Makros kann durchaus zu Problemen führen.

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

    – Sandi Hrvic

    26. Dezember 2012 um 14:04 Uhr

1647193216 612 Warum sind Praprozessor Makros bose und was sind die Alternativen
indiangarg

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

  • Erstellen verschiedener Releases derselben Software mithilfe von Konstrukten vom Typ #ifdef, z. B. das Release von Fenstern für verschiedene Regionen.
  • Zum Definieren von Codetest-bezogenen Werten.

Alternativen-
Für ähnliche Zwecke kann man eine Art von Konfigurationsdateien im ini-, xml-, json-Format verwenden. Ihre Verwendung hat jedoch Laufzeiteffekte auf den Code, die ein Präprozessormakro vermeiden kann.

  • seit C++17 constexpr if + eine Header-Datei, die “config” constexpr-Variablen enthält, kann die #ifdefs ersetzen.

    – Enhex

    19. September 2019 um 17:06 Uhr

998560cookie-checkWarum sind Präprozessor-Makros böse und was sind die Alternativen?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy