Verwenden von #ifdef, um während der Entwicklung zwischen verschiedenen Verhaltenstypen zu wechseln

28

Ist es empfehlenswert, #ifdef während der Entwicklung zu verwenden, um zwischen verschiedenen Verhaltenstypen zu wechseln? Zum Beispiel möchte ich das Verhalten von vorhandenem Code ändern, ich habe verschiedene Ideen, wie ich das Verhalten ändern kann, und es ist erforderlich, zwischen verschiedenen Implementierungen zu wechseln, um verschiedene Ansätze zu testen und zu vergleichen. Normalerweise sind Änderungen im Code komplex und wirken sich auf verschiedene Methoden in verschiedenen Dateien aus.

Normalerweise füge ich mehrere Bezeichner ein und mache so etwas

void foo()
{
    doSomething1();
#ifdef APPROACH1
    foo_approach1();
#endif
    doSomething2();
#ifdef APPROACH2
    foo_approach2();
#endif
}

void bar()
{
    doSomething3();
#ifndef APPROACH3
    doSomething4();
#endif
    doSomething5();
#ifdef APPROACH2
    bar_approach2();
#endif
}

int main()
{
    foo();
    bar();
    return 0;
}

Dies ermöglicht es, schnell zwischen verschiedenen Ansätzen zu wechseln und alles in nur einer Kopie des Quellcodes zu erledigen. Ist es ein guter Ansatz für die Entwicklung oder gibt es eine bessere Praxis?

Yury Shkliaryk
quelle
2
Da Sie über Entwicklung sprechen, müssen Sie meines Erachtens alles tun, was Sie leicht tun können, um mit verschiedenen Implementierungen zu wechseln und zu experimentieren. Dies entspricht eher den persönlichen Vorlieben während der Entwicklung, nicht den besten Methoden zur Lösung eines bestimmten Problems.
Emerson Cardoso
1
Ich würde empfehlen, das Strategiemuster oder den guten alten Polymorphismus zu verwenden, da dies dazu beiträgt, einen einzelnen Plug-In-Punkt für das umschaltbare Verhalten beizubehalten.
20.12.17
4
Beachten Sie, dass einige IDEs nichts in #ifdefBlöcken auswerten, wenn der Block deaktiviert ist. Wir sind auf Fälle gestoßen, in denen Code leicht veralten und nicht kompiliert werden kann, wenn Sie nicht routinemäßig alle Pfade erstellen.
Berin Loritsch
Schauen Sie sich diese Antwort an, die ich auf eine andere Frage gegeben habe. Es werden einige Möglichkeiten aufgezeigt, um vieles #ifdefsweniger umständlich zu machen .
user1118321

Antworten:

9

Ich würde es vorziehen, Versionskontrollzweige für diesen Anwendungsfall zu verwenden. Auf diese Weise können Sie zwischen den Implementierungen unterscheiden, für jede einen eigenen Verlauf verwalten und, wenn Sie Ihre Entscheidung getroffen haben und eine der Versionen entfernen müssen, diesen Zweig einfach verwerfen, anstatt eine fehleranfällige Bearbeitung durchzuführen.

Karl Bielefeldt
quelle
gitist besonders geschickt in so etwas. Vielleicht nicht so sehr der Fall svn, hgoder andere, aber es immer noch getan werden kann.
Twalberg
Das war auch mein erster Gedanke. "Willst du mit etwas anderem rumspielen?" git branch!
Wes Toleman
42

Wenn Sie einen Hammer halten, sieht alles aus wie ein Nagel. Es ist verlockend, wenn Sie wissen, wie es #ifdeffunktioniert, wenn Sie es als eine Art Mittel verwenden, um ein benutzerdefiniertes Verhalten in Ihrem Programm zu erzielen. Ich weiß, weil ich den gleichen Fehler gemacht habe.

Ich habe ein in MFC C ++ geschriebenes Legacy-Programm geerbt, mit dem bereits #ifdefplattformspezifische Werte definiert wurden. Dies bedeutete, dass ich mein Programm kompilieren konnte, um es auf einer 32-Bit-Plattform oder einer 64-Bit-Plattform zu verwenden, indem ich bestimmte Makrowerte definierte (oder in einigen Fällen nicht definierte).

Das Problem trat dann auf, dass ich benutzerdefiniertes Verhalten für einen Client schreiben musste. Ich hätte eine Verzweigung erstellen und eine separate Codebasis für den Client erstellen können, aber das hätte die Wartung zur Hölle gemacht. Ich hätte auch Konfigurationswerte definieren können, die vom Programm beim Start gelesen werden sollen, und diese Werte zum Bestimmen des Verhaltens verwenden müssen, aber dann müsste ich benutzerdefinierte Setups erstellen, um den Konfigurationsdateien für jeden Client die richtigen Konfigurationswerte hinzuzufügen.

Ich war versucht und gab nach. Ich schrieb #ifdefAbschnitte in meinen Code, um die verschiedenen Verhaltensweisen zu unterscheiden. Machen Sie keinen Fehler, es war zunächst nichts übertrieben. Es wurden sehr geringfügige Verhaltensänderungen vorgenommen, die es mir ermöglichten, Versionen des Programms an unsere Kunden weiterzugeben, und ich brauche nicht mehr als eine Version der Codebasis.

Im Laufe der Zeit wurde dies sowieso zur Wartungshölle, da sich das Programm nicht mehr durchgehend konsistent verhielt. Wenn ich eine Version des Programms testen wollte, musste ich unbedingt wissen, wer der Client war. Obwohl ich versucht habe, den Code auf eine oder zwei Header-Dateien zu reduzieren, war er sehr unübersichtlich, und aufgrund des schnellen #ifdefLösungsansatzes verbreiteten sich solche Lösungen im gesamten Programm wie ein bösartiger Krebs.

Ich habe seitdem meine Lektion gelernt und du solltest es auch. Verwenden Sie es, wenn Sie es unbedingt müssen, und verwenden Sie es ausschließlich für Plattformänderungen. Der beste Weg, um auf Verhaltensunterschiede zwischen Programmen (und damit Clients) zu reagieren, besteht darin, nur die beim Start geladene Konfiguration zu ändern. Das Programm bleibt konsistent und es ist sowohl leichter zu lesen als auch zu debuggen.

Neil
quelle
Was ist mit Debug-Versionen, wie "Wenn Debug die Variable x definiert ..."? Dies scheint nützlich für Dinge wie das Protokollieren zu sein, aber es könnte auch die Funktionsweise Ihres Programms grundlegend verändern, wenn Debug aktiviert ist und wenn dies nicht der Fall ist .
am
8
@ Snb Ich dachte darüber nach. Ich bevorzuge es weiterhin, eine Konfigurationsdatei zu ändern und sie detaillierter protokollieren zu lassen. Andernfalls geht etwas mit dem Programm in der Produktion schief und Sie haben keine Möglichkeit, es zu debuggen, ohne die ausführbare Datei vollständig zu ersetzen. Dies ist selbst in idealen Situationen weniger wünschenswert. ;)
Neil
Oh ja, es wäre viel idealer, nicht neu kompilieren zu müssen, um zu debuggen, darüber habe ich nicht nachgedacht!
am
9
Ein extremes Beispiel für das, was Sie beschreiben, finden Sie im zweiten Absatz unter der Überschrift "Das Problem der Wartbarkeit" in diesem Artikel darüber, warum MS den Punkt erreicht hat, an dem sie den größten Teil ihrer C-Laufzeit vor einigen Jahren von Grund auf neu schreiben mussten . blogs.msdn.microsoft.com/vcblog/2014/06/10/…
Dan Neely
2
@snb Die meisten Protokollierungsbibliotheken setzen einen Protokollierungsstufenmechanismus voraus. Wenn Sie möchten, dass bestimmte Informationen während des Debuggens protokolliert werden, protokollieren Sie diese mit einer niedrigen Protokollierungsstufe (normalerweise "Debuggen" oder "Ausführlich"). Die Anwendung verfügt dann über einen Konfigurationsparameter , der angibt, welche Ebene protokolliert werden soll. Die Antwort ist also noch Konfiguration für dieses Problem. Dies hat auch den enormen Vorteil, dass diese niedrige Protokollierungsstufe in einer Clientumgebung aktiviert werden kann .
jpmc26
21

Vorübergehend ist nichts falsch an dem, was Sie tun (etwa vor dem Einchecken): Es ist eine großartige Möglichkeit, verschiedene Kombinationen von Techniken zu testen oder einen Codeabschnitt zu ignorieren (obwohl dies von Problemen an und für sich spricht).

Aber ein Wort der Warnung: Halten Sie keine #ifdef-Zweige da, es ist wenig frustrierender, als meine Zeit damit zu verschwenden, dasselbe auf vier verschiedene Arten zu lesen , nur um herauszufinden, welche ich lesen sollte .

Das Lesen eines #ifdef ist mühsam, da Sie sich tatsächlich daran erinnern müssen, es zu überspringen! Machen Sie es sich nicht schwerer, als es unbedingt sein muss.

Verwenden Sie #ifdefs so sparsam wie möglich. In der Regel können Sie dies in Ihrer Entwicklungsumgebung für permanente Unterschiede wie Debug- / Release-Builds oder für verschiedene Architekturen tun .

Ich habe Bibliotheksfunktionen geschrieben, die von eingeschlossenen Bibliotheksversionen abhängig waren, für die #ifdef-Teilungen erforderlich waren. Manchmal mag es der einzige oder der einfachste Weg sein, aber selbst dann sollten Sie sich darüber aufregen, sie zu behalten.

Erdrik Ironrose
quelle
1

Die Verwendung von #ifdefs macht das Lesen von Code sehr schwierig.

Also, nein, benutze nicht so #ifdefs.

Es kann Unmengen von Argumenten geben, warum man ifdefs nicht verwendet, für mich ist dies genug.

void foo()
{
    doSomething1();
#ifdef APPROACH1
    foo_approach1();
#endif
    doSomething2();
#ifdef APPROACH2
    foo_approach2();
#endif
}

Kann eine Menge Dinge tun, die es tun kann:

void foo()
{
    doSomething1();
    doSomething2();
}

void foo()
{
    doSomething1();
    foo_approach1();
    doSomething2();
}

void foo()
{
    doSomething1();
    doSomething2();
    foo_approach2();
}

void foo()
{
    doSomething1();
    foo_approach1();
    doSomething2();
    foo_approach2();
}

Alles abhängig davon, welche Ansätze definiert sind oder nicht. Was es tut, ist auf den ersten Blick absolut nicht klar.

Pieter B
quelle