Makroschaden für C ++ - Code

verändert

Die C ++ - Sprache bietet enorme Möglichkeiten, auf Makros zu verzichten. Versuchen wir also, so wenig Makros wie möglich zu verwenden!

Machen Sie sofort einen Vorbehalt, dass ich kein Fanatiker bin und nicht darauf dränge, Makros aus idealistischen Gründen aufzugeben. Wenn es beispielsweise darum geht, denselben Codetyp manuell zu generieren, kann ich die Vorteile von Makros erkennen und mich damit abfinden. Zum Beispiel bin ich ruhig in Bezug auf Makros in alten Programmen, die mit MFC geschrieben wurden. Es macht keinen Sinn, mit so etwas zu kämpfen:

BEGIN_MESSAGE_MAP(efcDialog, EFCDIALOG_PARENT ) //{{AFX_MSG_MAP(efcDialog) ON_WM_CREATE() ON_WM_DESTROY() //}}AFX_MSG_MAP END_MESSAGE_MAP() 

Es gibt solche Makros und okay. Sie wurden wirklich geschaffen, um die Programmierung zu vereinfachen.

Ich spreche von anderen Makros, mit denen sie versuchen, die Implementierung einer vollwertigen Funktion zu vermeiden oder die Größe einer Funktion zu reduzieren. Betrachten Sie verschiedene Motive, um solche Makros zu vermeiden.

Hinweis Dieser Text wurde als Gastbeitrag für das Simplify C ++ - Blog verfasst. Ich habe beschlossen, die russische Version des Artikels hier zu veröffentlichen. Eigentlich schreibe ich diese Notiz, um eine Frage von unaufmerksamen Lesern zu vermeiden, warum der Artikel nicht als "Übersetzung" markiert ist :). Und hier eigentlich ein Gastbeitrag auf Englisch: " Macro Evil in C ++ Code ".

Erstens: Makrocode zieht Fehler an


Ich weiß nicht, wie ich die Gründe für dieses Phänomen aus philosophischer Sicht erklären soll, aber es ist so. Darüber hinaus sind makrobezogene Fehler bei der Durchführung einer Codeüberprüfung oft sehr schwer zu erkennen.

Ich habe solche Fälle in meinen Artikeln wiederholt beschrieben. Beispiel: Ersetzen der isspace- Funktion durch dieses Makro:

 #define isspace(c) ((c)==' ' || (c) == '\t') 

Der Programmierer, der isspace verwendete, glaubte, eine echte Funktion zu verwenden, die nicht nur Leerzeichen und Tabulatoren als Leerzeichen betrachtet, sondern auch LF, CR usw. Das Ergebnis ist, dass eine der Bedingungen immer erfüllt ist und der Code nicht wie beabsichtigt funktioniert. Dieser Fehler von Midnight Commander wird hier beschrieben.

Oder wie gefällt Ihnen diese Abkürzung zum Schreiben der Funktion std :: printf ?

 #define sprintf std::printf 

Ich denke, der Leser vermutet, dass es ein sehr erfolgloses Makro war. Es wurde übrigens im StarEngine-Projekt gefunden. Lesen Sie hier mehr darüber.

Man könnte argumentieren, dass Programmierer für diese Fehler verantwortlich sind, nicht für Makros. Ist das so. Natürlich sind Programmierer immer für Fehler verantwortlich :).

Es ist wichtig, dass Makros Fehler verursachen. Es stellt sich heraus, dass Makros mit erhöhter Genauigkeit oder überhaupt nicht verwendet werden müssen.

Ich kann Beispiele für Fehler nennen, die mit der Verwendung von Makros seit langem verbunden sind, und diese nette Notiz wird zu einem gewichtigen mehrseitigen Dokument. Natürlich werde ich das nicht tun, aber ich werde ein paar andere Fälle zeigen, um zu überzeugen.

Die ATL-Bibliothek bietet Makros wie A2W, T2W usw. zum Konvertieren von Zeichenfolgen. Nur wenige Menschen wissen jedoch, dass die Verwendung dieser Makros in Schleifen sehr gefährlich ist. Innerhalb des Makros wird die Zuordnungsfunktion aufgerufen, die bei jeder Iteration der Schleife immer wieder Speicher auf dem Stapel zuweist . Ein Programm gibt möglicherweise vor, korrekt zu funktionieren. Sobald das Programm mit der Verarbeitung langer Zeilen beginnt oder die Anzahl der Iterationen in der Schleife zunimmt, kann der Stapel im unerwartetsten Moment dauern und enden. Weitere Informationen hierzu finden Sie in diesem Minibuch (siehe Kapitel „Rufen Sie die Funktion alloca () nicht in Schleifen auf“).

Makros wie A2W verbergen das Böse. Sie sehen aus wie Funktionen, haben aber tatsächlich Nebenwirkungen, die schwer zu bemerken sind.

Ich komme an ähnlichen Versuchen, Code mithilfe von Makros zu reduzieren, nicht vorbei:

 void initialize_sanitizer_builtins (void) { .... #define DEF_SANITIZER_BUILTIN(ENUM, NAME, TYPE, ATTRS) \ decl = add_builtin_function ("__builtin_" NAME, TYPE, ENUM, \ BUILT_IN_NORMAL, NAME, NULL_TREE); \ set_call_expr_flags (decl, ATTRS); \ set_builtin_decl (ENUM, decl, true); #include "sanitizer.def" if ((flag_sanitize & SANITIZE_OBJECT_SIZE) && !builtin_decl_implicit_p (BUILT_IN_OBJECT_SIZE)) DEF_SANITIZER_BUILTIN (BUILT_IN_OBJECT_SIZE, "object_size", BT_FN_SIZE_CONST_PTR_INT, ATTR_PURE_NOTHROW_LEAF_LIST) .... } 

Nur die erste Zeile des Makros bezieht sich auf die if-Anweisung . Die restlichen Zeilen werden unabhängig von der Bedingung ausgeführt. Wir können sagen, dass dieser Fehler aus der C-Welt stammt, da er von mir mithilfe der V640- Diagnose im GCC-Compiler gefunden wurde. GCC-Code wird hauptsächlich in C geschrieben, und in dieser Sprache sind Makros schwer zu erstellen. Sie müssen jedoch zugeben, dass dies nicht der Fall ist. Hier war es durchaus möglich, eine echte Funktion zu machen.

Zweitens: Das Lesen von Code wird komplizierter


Wenn Sie auf ein Projekt stoßen, das voller Makros ist, die aus anderen Makros bestehen, dann verstehen Sie, was zum Teufel es ist, ein solches Projekt zu verstehen. Wenn Sie nicht angetroffen haben, dann nehmen Sie ein Wort, das ist traurig. Als Beispiel für Code, der schwer zu lesen ist, kann ich den zuvor erwähnten GCC-Compiler zitieren.

Der Legende nach hat Apple aufgrund der Komplexität des GCC-Codes aufgrund dieser Makros in die Entwicklung des LLVM-Projekts als Alternative zu GCC investiert. Wo ich darüber gelesen habe, erinnere ich mich nicht, daher wird es keine Beweise geben.

Drittens: Makros zu schreiben ist schwierig


Es ist einfach, ein schlechtes Makro zu schreiben. Ich treffe sie überall mit entsprechenden Konsequenzen. Das Schreiben eines guten und zuverlässigen Makros ist jedoch oft schwieriger als das Schreiben einer ähnlichen Funktion.

Das Schreiben eines guten Makros ist schwierig, da es im Gegensatz zu einer Funktion nicht als unabhängige Einheit betrachtet werden kann. Es ist erforderlich, das Makro sofort im Kontext aller möglichen Optionen für seine Verwendung zu betrachten, da es sonst sehr einfach ist, ein Problem des Formulars zu lösen:

 #define MIN(X, Y) (((X) < (Y)) ? (X) : (Y)) m = MIN(ArrayA[i++], ArrayB[j++]); 

In solchen Fällen wurden natürlich schon lange Problemumgehungen erfunden, und das Makro kann sicher implementiert werden:

 #define MAX(a,b) \ ({ __typeof__ (a) _a = (a); \ __typeof__ (b) _b = (b); \ _a > _b ? _a : _b; }) 

Die Frage ist nur, brauchen wir das alles in C ++? Nein, in C ++ gibt es Vorlagen und andere Möglichkeiten, um effizienten Code zu erstellen. Warum stoße ich in C ++ - Programmen weiterhin auf ähnliche Makros?

Viertens: Das Debuggen ist kompliziert


Es gibt eine Meinung, dass das Debuggen für Weicheier ist :). Dies ist natürlich interessant zu diskutieren, aber aus praktischer Sicht ist das Debuggen nützlich und hilft, Fehler zu finden. Makros erschweren diesen Prozess und verlangsamen definitiv die Suche nach Fehlern.

Fünftens: Fehlalarme von statischen Analysatoren


Viele Makros erzeugen aufgrund der Besonderheiten ihres Geräts mehrere Fehlalarme von statischen Code-Analysatoren. Ich kann mit Sicherheit sagen, dass die meisten Fehlalarme beim Überprüfen von C- und C ++ - Code mit Makros verknüpft sind.

Das Problem mit Makros ist, dass die Analysatoren den richtigen kniffligen Code einfach nicht vom fehlerhaften Code unterscheiden können. Der Artikel zum Überprüfen von Chromium beschreibt eines dieser Makros.

Was zu tun ist?


Verwenden wir keine Makros in C ++ - Programmen, es sei denn, dies ist unbedingt erforderlich!

C ++ bietet umfangreiche Tools wie Vorlagenfunktionen, automatische Typinferenz (auto, decltype) und constexpr-Funktionen.

Fast immer können Sie anstelle eines Makros eine normale Funktion schreiben. Oft wird dies wegen gewöhnlicher Faulheit nicht getan. Diese Faulheit ist schädlich, und wir müssen sie bekämpfen. Eine kleine zusätzliche Zeit, die für das Schreiben einer vollwertigen Funktion aufgewendet wird, zahlt sich mit Zinsen aus. Der Code ist einfacher zu lesen und zu warten. Die Wahrscheinlichkeit, auf Ihr eigenes Bein zu schießen, nimmt ab, und Compiler und statische Analysatoren erzeugen weniger Fehlalarme.

Einige könnten argumentieren, dass Code mit einer Funktion weniger effizient ist. Dies ist auch nur eine "Ausrede".

Compiler integrieren den Code jetzt perfekt, auch wenn Sie das Inline-Schlüsselwort nicht geschrieben haben.

Wenn wir über die Berechnung von Ausdrücken in der Kompilierungsphase sprechen, werden hier keine Makros benötigt und sind sogar schädlich. Für den gleichen Zweck ist es viel besser und sicherer, constexpr zu verwenden.

Ich werde mit einem Beispiel erklären. Hier ist ein klassischer Makrofehler, den ich aus dem FreeBSD-Kernel-Code ausgeliehen habe.

 #define ICB2400_VPOPT_WRITE_SIZE 20 #define ICB2400_VPINFO_PORT_OFF(chan) \ (ICB2400_VPINFO_OFF + \ sizeof (isp_icb_2400_vpinfo_t) + \ (chan * ICB2400_VPOPT_WRITE_SIZE)) // <= static void isp_fibre_init_2400(ispsoftc_t *isp) { .... if (ISP_CAP_VP0(isp)) off += ICB2400_VPINFO_PORT_OFF(chan); else off += ICB2400_VPINFO_PORT_OFF(chan - 1); // <= .... } 

Das chan- Argument wird in einem Makro verwendet, ohne in Klammern eingeschlossen zu werden. Infolgedessen multipliziert der Ausdruck ICB2400_VPOPT_WRITE_SIZE nicht den Ausdruck (chan - 1) , sondern nur einen.

Der Fehler wird nicht angezeigt, wenn anstelle eines Makros eine normale Funktion geschrieben wurde.

 size_t ICB2400_VPINFO_PORT_OFF(size_t chan) { return ICB2400_VPINFO_OFF + sizeof(isp_icb_2400_vpinfo_t) + chan * ICB2400_VPOPT_WRITE_SIZE; } 

Es ist sehr wahrscheinlich, dass der moderne C- und C ++ - Compiler das Inlining der Funktion unabhängig voneinander ausführt und der Code genauso effizient ist wie im Fall eines Makros.

Gleichzeitig wurde der Code lesbarer und fehlerfrei.

Wenn bekannt ist, dass der Eingabewert immer eine Konstante ist, können Sie constexpr hinzufügen und sicherstellen, dass alle Berechnungen in der Kompilierungsphase ausgeführt werden. Stellen Sie sich vor, es ist C ++ und Chan ist immer eine Konstante. Dann ist es nützlich, die Funktion ICB2400_VPINFO_PORT_OFF wie folgt zu deklarieren:

 constexpr size_t ICB2400_VPINFO_PORT_OFF(size_t chan) { return ICB2400_VPINFO_OFF + sizeof(isp_icb_2400_vpinfo_t) + chan * ICB2400_VPOPT_WRITE_SIZE; } 

Gewinn!

Ich hoffe ich habe es geschafft dich zu überzeugen. Viel Glück und weniger Makros im Code!

Source: https://habr.com/ru/post/de444612/


All Articles