Das Weglassen von "Destruktoren" in C bringt YAGNI zu weit?

9

Ich arbeite an einer mittelgroßen eingebetteten Anwendung in C mit OO-ähnlichen Techniken. Meine "Klassen" sind .h / .c-Module, die Datenstrukturen und Funktionszeigerstrukturen verwenden, um Kapselung, Polymorphismus und Abhängigkeitsinjektion zu emulieren.

Nun würde man erwarten, dass eine myModule_create(void)Funktion mit einem myModule_destroy(pointer)Gegenstück kommt. Da das Projekt eingebettet ist, sollten die Ressourcen, die realistisch instanziiert werden, niemals freigegeben werden.

Ich meine, wenn ich 4 serielle UART-Ports habe und 4 UART-Instanzen mit den erforderlichen Pins und Einstellungen erstelle, gibt es absolut keinen Grund, UART # 2 irgendwann zur Laufzeit jemals zerstören zu wollen.

Sollte ich nach dem YAGNI-Prinzip (Du wirst es nicht brauchen) die Destruktoren weglassen? Das scheint mir äußerst seltsam, aber ich kann mir keine Verwendung für sie vorstellen. Ressourcen werden beim Ausschalten des Geräts freigegeben.

Asics
quelle
4
Wenn Sie keine Möglichkeit zum Entsorgen des Objekts angeben, geben Sie eine eindeutige Nachricht weiter, dass das Objekt nach seiner Erstellung eine "unendliche" Lebensdauer hat. Wenn dies für Ihre Bewerbung sinnvoll ist, sage ich: Tun Sie es.
Glampert
4
Wenn Sie den Typ so weit an Ihren speziellen Anwendungsfall koppeln möchten, warum sollten Sie dann überhaupt eine myModule_create(void)Funktion haben? Sie können die spezifischen Instanzen, die Sie voraussichtlich verwenden, in der von Ihnen bereitgestellten Schnittstelle fest codieren.
Doval
@Doval Ich habe darüber nachgedacht. Ich bin ein Praktikant, der Teile und Teile des Codes meines Vorgesetzten verwendet, also versuche ich, mit "es richtig zu machen" zu jonglieren, den OO-Stil in C zu experimentieren, um Erfahrung zu sammeln und mit den Unternehmensstandards übereinzustimmen.
Asics
2
@glampert nagelt es; Ich möchte hinzufügen, dass Sie die erwartete unendliche Lebensdauer in der Dokumentation der Erstellungsfunktion bereinigen sollten.
Blrfl

Antworten:

11

Wenn Sie keine Möglichkeit zum Entsorgen des Objekts angeben, geben Sie eine eindeutige Nachricht weiter, dass das Objekt nach seiner Erstellung eine "unendliche" Lebensdauer hat. Wenn dies für Ihre Bewerbung sinnvoll ist, sage ich: Tun Sie es.

Glampert ist richtig; Hier werden keine Destruktoren benötigt. Sie würden nur Code aufblähen und eine Gefahr für Benutzer darstellen (die Verwendung eines Objekts nach dem Aufruf seines Destruktors ist undefiniertes Verhalten).

Sie sollten jedoch sicher sein, dass die Objekte wirklich nicht entsorgt werden müssen. Benötigen Sie beispielsweise ein Objekt für einen UART, das derzeit nicht verwendet wird?

Demi
quelle
3

Der einfachste Weg, Speicherlecks zu erkennen, besteht darin, Ihre Anwendung sauber zu beenden. Viele Compiler / Umgebungen bieten eine Möglichkeit, nach Speicher zu suchen, der beim Beenden Ihrer Anwendung noch zugewiesen ist. Wenn einer nicht bereitgestellt wird, gibt es normalerweise eine Möglichkeit, direkt vor dem Beenden Code hinzuzufügen, um dies herauszufinden.

Daher würde ich sicherlich Konstruktoren, Destruktoren und Abschaltlogik bereitstellen, selbst in einem eingebetteten System, das "theoretisch" niemals beendet werden sollte, um die Erkennung von Speicherlecks allein zu vereinfachen. Tatsächlich ist die Erkennung von Speicherverlusten noch wichtiger, wenn der Anwendungscode niemals beendet werden soll.

Dunk
quelle
Beachten Sie, dass dies nicht gilt, wenn Sie Zuweisungen nur während des Startvorgangs vornehmen. Dies ist ein Muster, das ich bei Geräten mit Speicherbeschränkungen ernsthaft berücksichtigen würde.
CodesInChaos
@Codes: Es gibt keinen Grund, sich einzuschränken. Während Sie sich vielleicht das großartige Design einfallen lassen, das beim Start Speicher vorab zuweist, werden die Leute nach Ihnen, die nicht in dieses großartige Schema eingeweiht sind oder dessen Bedeutung nicht erkennen, Speicher auf dem Programm zuweisen fliegen und da geht dein Design. Machen Sie es einfach richtig und weisen Sie zu / entfernen Sie die Zuordnung und überprüfen Sie, ob das, was Sie implementiert haben, tatsächlich funktioniert. Wenn Sie wirklich über ein Gerät mit eingeschränktem Speicher verfügen, müssen Sie normalerweise die neuen Operator- / Malloc- und Vorreservierungszuweisungsblöcke überschreiben.
Dunk
3

In meiner Entwicklung, bei der undurchsichtige Datentypen in großem Umfang verwendet werden, um einen OO-ähnlichen Ansatz zu fördern, habe ich mich ebenfalls mit dieser Frage auseinandergesetzt. Zuerst war ich entschieden im Lager, den Destruktor aus der YAGNI-Perspektive sowie der MISRA-Perspektive "Dead Code" zu eliminieren. (Ich hatte viel Platz für Ressourcen, das war keine Überlegung.)

Das Fehlen eines Destruktors kann jedoch das Testen erschweren, wie beim automatisierten Testen von Einheiten / Integrationen. Herkömmlicherweise sollte jeder Test ein Setup / Teardown unterstützen, damit Objekte erstellt, manipuliert und dann zerstört werden können. Sie werden zerstört, um einen sauberen, unbefleckten Ausgangspunkt für den nächsten Test zu gewährleisten. Dazu benötigt die Klasse einen Destruktor.

Meiner Erfahrung nach ist das "aint't" in YAGNI daher ein "are" und ich habe Destruktoren für jede Klasse erstellt, unabhängig davon, ob ich dachte, ich würde es brauchen oder nicht. Selbst wenn ich das Testen übersprungen habe, gibt es mindestens einen korrekt entworfenen Destruktor für den armen Slob, der mir folgt, der einen hat. (Und zu einem viel geringeren Wert macht es den Code wiederverwendbarer, da er in einer Umgebung verwendet werden kann, in der er zerstört werden würde.)

Während dies YAGNI adressiert, adressiert es keinen toten Code. Dafür finde ich, dass ein bedingtes Kompilierungsmakro wie #define BUILD_FOR_TESTING es ermöglicht, den Destruktor aus dem endgültigen Produktionsbuild zu entfernen.

Gehen Sie dazu folgendermaßen vor: Sie haben einen Destruktor zum Testen / zur zukünftigen Wiederverwendung und erfüllen die Entwurfsziele von YAGNI und die Regeln für "kein toter Code".

Greg Willits
quelle
Seien Sie vorsichtig, wenn Sie Ihren Test- / Produktcode definieren. Es ist ziemlich sicher, wenn es auf eine ganze Funktion angewendet wird, wie Sie beschreiben, denn wenn die Funktion tatsächlich benötigt wird, schlägt die Kompilierung fehl. Die Verwendung von #ifdef inline innerhalb einer Funktion ist jedoch viel riskanter, da Sie jetzt einen anderen Codepfad testen als in prod.
Kevin
0

Sie könnten einen No-Op-Destruktor haben, so etwas wie

  void noop_destructor(void*) {};

Stellen Sie dann den Destruktor ein, der Uartmöglicherweise verwendet wird

  #define Uart_destructor noop_destructor

(Fügen Sie bei Bedarf die geeignete Besetzung hinzu.)

Vergessen Sie nicht zu dokumentieren. Vielleicht willst du sogar

 #define Uart_destructor abort

Alternativ kann ein Sonderfall im allgemeinen Code, der den Destruktor aufruft, der Fall sein, in dem die Destruktorzeigerfunktion NULLdas Aufrufen vermeiden soll.

Basile Starynkevitch
quelle