Ist es besser, die Präprozessor-Direktive oder die if-Anweisung (Konstante) zu verwenden?

10

Angenommen, wir haben eine Codebasis, die für viele verschiedene Kunden verwendet wird, und wir haben Code, der nur für Kunden vom Typ X relevant ist. Ist es besser, Präprozessoranweisungen zu verwenden, um diesen Code nur in Kunden vom Typ X aufzunehmen, oder if-Anweisungen verwenden? Um klarer zu sein:

// some code
#if TYPE_X_COSTUMER  = 1
// do some things
#endif
// rest of the code

oder

if(TYPE_X_COSTUMER) {
    // do some things
}

Die Argumente, über die ich nachdenken kann, sind:

  • Die Präprozessor-Direktive führt zu einem geringeren Code-Footprint und weniger Verzweigungen (bei nicht optimierenden Compilern).
  • Wenn Anweisungen zu Code führen, der immer kompiliert wird, z. B. wenn jemand einen Fehler macht, der den irrelevanten Code für das Projekt, an dem er arbeitet, beschädigt, wird der Fehler weiterhin angezeigt und die Codebasis wird nicht beschädigt. Andernfalls wird ihm die Korruption nicht bekannt sein.
  • Mir wurde immer gesagt, ich solle die Verwendung des Prozessors der Verwendung des Präprozessors vorziehen (wenn dies überhaupt ein Argument ist ...)

Was ist vorzuziehen - wenn es um eine Codebasis für viele verschiedene Kunden geht?

MByD
quelle
3
Wie hoch ist die Wahrscheinlichkeit, dass Sie einen Build versenden, der nicht mit einem optimierenden Compiler erstellt wurde? Und wenn es passiert, spielt die Auswirkung eine Rolle (insbesondere im Zusammenhang mit all den anderen Optimierungen, die Sie dann verpassen werden)?
@delnan - Der Code wird für viele verschiedene Plattformen mit vielen verschiedenen Compilern verwendet. Und in Bezug auf die Auswirkungen - es könnte in bestimmten Fällen.
MByD
Ihnen wurde gesagt, Sie sollen etwas bevorzugen? Das ist so, als würde man jemanden zwingen, Essen vom KFC zu essen, während er kein Huhn mag.
Rechtsfalte
@WTP - nein, war ich nicht, ich bin daran interessiert, mehr zu wissen, damit ich in Zukunft bessere Entscheidungen treffen kann.
MByD
Ist in Ihrem zweiten Beispiel TYPE_X_CUSTOMERnoch ein Präprozessor-Makro?
Detly

Antworten:

5

Ich denke, es gibt einen Vorteil bei der Verwendung eines #define, den Sie nicht erwähnt haben, und das ist die Tatsache, dass Sie den Wert in der Befehlszeile festlegen können (also in Ihrem einstufigen Build-Skript festlegen).

Ansonsten ist es im Allgemeinen besser, Makros zu vermeiden. Sie respektieren kein Scoping und das kann Probleme verursachen. Nur sehr dumme Compiler können eine Bedingung basierend auf einer Kompilierungszeitkonstante nicht optimieren. Ich weiß nicht, ob dies ein Problem für Ihr Produkt darstellt (zum Beispiel kann es wichtig sein, den Code auf eingebetteten Plattformen klein zu halten).

Tamás Szelei
quelle
Eigentlich ist dies ist für eingebettete Systeme
MByD
Das Problem ist, dass eingebettete Systeme normalerweise einen alten Compiler verwenden (wie zum Beispiel gcc 2.95).
BЈовић
@VJo - GCC ist der glückliche Fall :)
MByD
Probieren Sie es dann aus. Wenn es den nicht verwendeten Code enthält und dieser eine beträchtliche Größe hat, gehen Sie zu den Definitionen.
Tamás Szelei
Wenn Sie es über die Compiler-Befehlszeile festlegen möchten, verwende ich immer noch eine Konstante im Code selbst und definiere nur den dieser Konstante zugewiesenen Wert.
CodesInChaos
12

Wie bei vielen Fragen hängt die Antwort auf diese Frage davon ab . Anstatt zu sagen, was besser ist, habe ich eher Beispiele und Ziele angegeben, bei denen eines besser ist als das andere.

Sowohl der Präprozessor als auch die Konstante haben ihre eigenen Verwendungsorte.

Im Falle eines Vorprozessors wird der Code vor der Kompilierungszeit entfernt. Daher ist es am besten für Situationen geeignet, in denen erwartet wird, dass kein Code kompiliert wird . Dies kann sich auf die Modulstruktur und die Abhängigkeiten auswirken und die Auswahl der besten Codesegmente für Leistungsaspekte ermöglichen. In den folgenden Fällen muss der Code nur mit einem Präprozessor geteilt werden.

  1. Multi-Plattform-Code:
    Zum Beispiel, wenn Code unter verschiedenen Plattformen kompiliert wird, wenn Code von bestimmten Versionsnummern des Betriebssystems abhängt (oder sogar von der Compiler-Version - obwohl dies sehr selten ist). Wenn Sie beispielsweise mit Little-Endien-Big-Endien-Gegenstücken von Code arbeiten, müssen diese eher mit Präprozessoren als mit Konstanten getrennt werden. Oder wenn Sie Code für Windows und Linux kompilieren und bestimmte Systemaufrufe sehr unterschiedlich sind.

  2. Experimentelle Patches:
    Ein anderer Fall, in dem dies gerechtfertigt ist, ist ein experimenteller Code, der riskant ist, oder bestimmte wichtige Module, die weggelassen werden müssen und signifikante Verknüpfungs- oder Leistungsunterschiede aufweisen. Der Grund, warum man den Code über den Präprozessor deaktivieren möchte, anstatt sich unter if () zu verstecken, liegt darin, dass wir uns möglicherweise nicht sicher sind, welche Fehler durch diesen speziellen Änderungssatz verursacht werden, und dass wir auf experimenteller Basis ausgeführt werden. Wenn dies fehlschlägt, müssen wir nichts anderes tun , als diesen Code in der Produktion zu deaktivieren , als ihn neu zu schreiben. Einige Zeit ist es ideal, um #if 0 den gesamten Code zu kommentieren.

  3. Umgang mit Abhängigkeiten:
    Ein weiterer Grund, aus dem Sie möglicherweise generieren möchten Wenn Sie beispielsweise keine JPEG-Bilder unterstützen möchten, können Sie das Kompilieren dieses Moduls / Stubs vermeiden, und die Bibliothek wird möglicherweise nicht (statisch oder dynamisch) mit diesem verknüpft Modul. Manchmal werden Pakete ausgeführt ./configure, um die Verfügbarkeit einer solchen Abhängigkeit zu ermitteln. Wenn keine Bibliotheken vorhanden sind (oder der Benutzer diese nicht aktivieren möchte), wird diese Funktionalität automatisch deaktiviert, ohne eine Verknüpfung mit dieser Bibliothek herzustellen. Hier ist es immer vorteilhaft, wenn diese Anweisungen automatisch generiert werden.

  4. Lizenzierung:
    Ein sehr interessantes Beispiel für eine Präprozessor-Direktive ist ffmpeg . Es verfügt über Codecs, die durch ihre Verwendung möglicherweise Patente verletzen können. Wenn Sie die Quelle herunterladen und zur Installation kompilieren, werden Sie gefragt, ob Sie solche Inhalte möchten oder nicht. Halten Sie Codes unter bestimmten Umständen versteckt, wenn die Bedingungen Sie immer noch vor Gericht bringen können!

  5. Code kopieren und einfügen:
    Aka-Makros. Dies ist kein Ratschlag für die übermäßige Verwendung von Makros - nur, dass Makros eine viel leistungsfähigere Möglichkeit bieten, das Äquivalent zum Kopieren nach Vergangenheit anzuwenden . Aber verwenden Sie es mit großer Sorgfalt; und verwenden Sie es, wenn Sie wissen, was Sie tun. Konstanten können das natürlich nicht. Man kann aber auch inlineFunktionen verwenden, wenn das einfach ist.

Wann verwenden Sie Konstanten?
Fast überall sonst.

  1. Ordentlicher Codefluss:
    Wenn Sie Konstanten verwenden, ist dieser im Allgemeinen kaum von regulären Variablen zu unterscheiden und daher besser lesbarer Code. Wenn Sie eine Routine mit 75 Zeilen schreiben, haben Sie 3 oder 4 Zeilen nach jeweils 10 Zeilen, wobei #ifdef SEHR nicht lesbar ist . Vermutlich eine primäre Konstante, die von #ifdef bestimmt wird, und sie überall in einem natürlichen Fluss verwenden.

  2. Gut eingerückter Code: Alle Präprozessor-Direktiven funktionieren niemals gut mit ansonsten gut eingerücktem Code . Selbst wenn Ihr Compiler das Einrücken von #def zulässt, hat der Pre-ANSI C-Präprozessor kein Leerzeichen zwischen dem Zeilenanfang und dem Zeichen "#" zugelassen. Das führende "#" musste immer in der ersten Spalte stehen.

  3. Konfiguration:
    Ein weiterer Grund, warum Konstanten / oder Variablen sinnvoll sind, besteht darin, dass sie sich leicht aus der Verknüpfung mit globalen Elementen entwickeln oder in Zukunft erweitert werden können, um aus Konfigurationsdateien abgeleitet zu werden.

Eine letzte Sache:
Verwenden Sie niemals Präprozessor-Anweisungen, #ifdefum #endif den Geltungsbereich zu überschreiten, oder { ... }. dh Beginn #ifdefoder Ende von #endifauf verschiedenen Seiten von { ... }. Das ist extrem schlecht; es kann verwirrend sein, es kann manchmal gefährlich sein.

Dies ist natürlich keine vollständige Liste, zeigt Ihnen jedoch einen deutlichen Unterschied, wo welche Methode besser geeignet ist. Es geht nicht wirklich darum, was besser ist , es geht immer mehr darum, welches in einem bestimmten Kontext natürlicher zu verwenden ist.

Dipan Mehta
quelle
Danke für die lange und ausführliche Antwort. Ich war mir der meisten Dinge bewusst, die Sie erwähnt haben, und sie wurden in der Frage nicht berücksichtigt, da es nicht darum geht, ob Präprozessorfunktionen überhaupt verwendet werden sollen, sondern um eine bestimmte Art der Verwendung. Aber ich denke, der grundlegende Punkt, den Sie gemacht haben, ist wahr.
MByD
1
"VERWENDEN Sie niemals Präprozessor-Direktiven #ifdef bis #endif, die den Bereich überschreiten, oder {...}", dies hängt von der IDE ab. Wenn IDE inaktive Präprozessorblöcke erkennt und sie reduziert oder mit einer anderen Farbe anzeigt, ist dies nicht verwirrend.
Abyx
@Abyx es ist wahr, dass IDE helfen kann. In vielen Fällen, in denen sich Benutzer unter der Plattform * nix (Linux usw.) entwickeln, gibt es jedoch viele Editoren usw. (VI, Emacs usw.). Daher verwendet möglicherweise jemand anderes einen anderen Editor als Ihren.
Dipan Mehta
4

Haben Ihre Kunden Zugriff auf Ihren Code? Wenn ja, könnte der Präprozessor eine bessere Option sein. Wir haben etwas Ähnliches und verwenden Kompilierungszeit-Flags für verschiedene Kunden (oder kundenspezifische Funktionen). Dann könnte ein Skript kundenspezifischen Code extrahieren und wir versenden diesen Code. Den Kunden sind andere Kunden oder andere kundenspezifische Merkmale nicht bekannt.

Aditya Sehgal
quelle
3

Einige weitere erwähnenswerte Punkte:

  1. Der Compiler kann einige Arten von konstanten Ausdrücken verarbeiten, die der Präprozessor nicht kann. Beispielsweise hat der Präprozessor im Allgemeinen keine Möglichkeit, sizeof()nicht-primitive Typen zu bewerten . Dies kann if()in einigen Fällen die Verwendung erzwingen .

  2. Der Compiler ignoriert die meisten Syntaxprobleme in Code, der übersprungen wird #if(einige Probleme im Zusammenhang mit Präprozessoren können immer noch Probleme verursachen), besteht jedoch auf syntaktischer Korrektheit für Code, mit dem übersprungen wird if(). Wenn also Code, der für einige Builds übersprungen wird, für andere jedoch nicht, infolge von Änderungen an anderer Stelle in der Datei ungültig wird (z. B. umbenannte Bezeichner), wird er im Allgemeinen bei allen Builds kreischen, wenn er deaktiviert ist, if()aber nicht, wenn er von deaktiviert ist #if. Je nachdem, warum der Code übersprungen wird, kann dies eine gute Sache sein oder auch nicht.

  3. Einige Compiler lassen nicht erreichbaren Code weg, andere nicht. Deklarationen der statischen Speicherdauer in nicht erreichbarem Code, einschließlich Zeichenfolgenliteralen, können jedoch Speicherplatz zuweisen, selbst wenn kein Code jemals den so zugewiesenen Speicherplatz verwenden kann.

  4. Makros können if()Tests in ihnen verwenden, aber es gibt leider keinen Mechanismus für den Präprozessor, um eine bedingte Logik innerhalb eines Makros auszuführen [Beachten Sie, dass der ? :Operator für die Verwendung in Makros oft besser ist als if, aber es gelten dieselben Prinzipien].

  5. Mit der #ifDirektive kann gesteuert werden, welche Makros definiert werden.

Ich denke, es if()ist in den meisten Fällen oft sauberer, außer in Fällen, in denen Entscheidungen getroffen werden, die darauf basieren, welchen Compiler verwendet wird oder welche Makros definiert werden sollten. Einige der oben genannten Probleme erfordern möglicherweise die Verwendung #if, selbst in Fällen, in denen ifdies sauberer erscheint.

Superkatze
quelle
1

Lange Rede, kurzer Sinn: Die Befürchtungen bezüglich Präprozessor-Direktiven sind im Grunde genommen 2, mehrere Einschlüsse und möglicherweise weniger lesbarer Code und eine kritischere Logik.

Wenn Ihr Projekt klein ist und fast keinen großen Plan für die Zukunft hat, bieten beide Optionen meines Erachtens das gleiche Verhältnis zwischen Vor- und Nachteilen. Wenn Ihr Projekt jedoch sehr groß sein wird, sollten Sie etwas anderes in Betracht ziehen, z. B. das Schreiben einer separaten Header-Datei, wenn Sie möchten weiterhin Präprozessor-Direktiven verwenden, denken Sie jedoch daran, dass diese Art von Ansatz den Code normalerweise weniger lesbar macht, und stellen Sie sicher, dass der Name dieser Konstanten etwas für die Geschäftslogik des Programms selbst bedeutet.

Mikro
quelle
Dies ist eine großartige Codebasis, und die Definitionen befinden sich tatsächlich in einigen separaten Headern.
MByD