Ist es sinnvoll, jeden einzelnen dereferenzierten Zeiger auf Null zu setzen?


65

Bei einem neuen Job wurde ich in Code-Reviews für Code wie diesen markiert:

PowerManager::PowerManager(IMsgSender* msgSender)
  : msgSender_(msgSender) { }

void PowerManager::SignalShutdown()
{
    msgSender_->sendMsg("shutdown()");
}

Mir wurde gesagt, dass die letzte Methode lauten sollte:

void PowerManager::SignalShutdown()
{
    if (msgSender_) {
        msgSender_->sendMsg("shutdown()");
    }
}

dh ich muss einen setzen NULLWache rund um die msgSender_Variable, auch wenn es ein privates Daten Mitglied ist. Es fällt mir schwer, mich davon abzuhalten, mit Sprengsätzen zu beschreiben, wie ich über dieses Stück „Weisheit“ stehe. Wenn ich nach einer Erklärung frage, bekomme ich eine ganze Reihe von Horrorgeschichten darüber, wie ein Junior-Programmierer eines Jahres verwirrt darüber wurde, wie eine Klasse funktionieren sollte, und löschte versehentlich ein Mitglied, das er nicht haben sollte (und setzte es NULLdanach auf (anscheinend) und die Dinge explodierten gleich nach einer Produktveröffentlichung, und wir haben "auf die harte Tour gelernt, vertraue uns", dass es besser ist, einfach alles zu NULLüberprüfen .

Für mich fühlt sich das schlicht und einfach nach Frachtkult-Programmierung an . Ein paar wohlmeinende Kollegen versuchen ernsthaft, mir zu helfen, es zu verstehen und zu sehen, wie mir das helfen wird, robusteren Code zu schreiben, aber ... ich kann nicht anders, als mich zu fühlen, als wären sie diejenigen, die es nicht verstehen .

Ist es für einen Codierungsstandard sinnvoll, zu verlangen, dass jeder einzelne Zeiger, der in einer Funktion dereferenziert wird, NULLzuerst auf private Datenelemente überprüft wird ? (Hinweis: Um einen gewissen Zusammenhang zu schaffen, stellen wir ein Unterhaltungselektronikgerät her, kein Flugsicherungssystem oder ein anderes Produkt, das dem Menschen gleichkommt.)

BEARBEITEN : Im obigen Beispiel ist der msgSender_Mitbearbeiter nicht optional. Wenn es jemals ist NULL, zeigt es einen Fehler an. Der einzige Grund, warum es an den Konstruktor übergeben wird, besteht darin, dass PowerManageres mit einer Scheinunterklasse getestet werden kann IMsgSender.

ZUSAMMENFASSUNG : Es gab einige wirklich gute Antworten auf diese Frage, danke an alle. Ich habe den von @aaronps hauptsächlich wegen seiner Kürze angenommen. Es scheint eine ziemlich breite allgemeine Übereinstimmung zu geben, dass:

  1. Mandatierung NULLSchutz für jeden einzelnen dereferenzierten Zeiger ist übertrieben, aber
  2. Sie können die gesamte Debatte durch Verwenden einer Referenz (falls möglich) oder eines constZeigers auslassen
  3. assertAnweisungen sind eine aufschlussreichere Alternative zu NULLGuards, um zu überprüfen, ob die Voraussetzungen für eine Funktion erfüllt sind.

14
Denken Sie nicht eine Minute lang, dass Fehler die Domäne von Junior-Programmierern sind. 95% der Entwickler aller Erfahrungsstufen haben ab und zu etwas falsch gemacht. Die restlichen 5% liegen durch die Zähne.
Blrfl

56
Wenn ich jemanden beim Schreiben von Code gesehen habe, der nach einer Null sucht und unbemerkt fehlschlägt, möchte ich ihn sofort abfeuern.
Winston Ewert

16
Stummes Versagen ist so ziemlich das Schlimmste. Zumindest wenn es explodiert, wissen Sie, wo die Explosion auftritt. Das Überprüfen auf nullund Nichtstun ist nur eine Möglichkeit, den Fehler während der Ausführung zu beheben, was es weitaus schwieriger macht, die Quelle zu finden.
MirroredFate

1
+1, sehr interessante Frage. Zusätzlich zu meiner Antwort möchte ich darauf hinweisen, dass Sie, wenn Sie Code schreiben müssen, der einen Komponententest durchführen soll, in Wirklichkeit ein erhöhtes Risiko für ein falsches Sicherheitsgefühl eingehen (mehr Codezeilen = mehr Möglichkeiten für Bugs). Das Neugestalten von Referenzen verbessert nicht nur die Lesbarkeit des Codes, sondern reduziert auch die Menge an Code (und Tests), die Sie schreiben müssen, um zu beweisen, dass dieser funktioniert.
Seth

6
@Rig, ich bin sicher, dass es geeignete Fälle für einen stillen Fehler gibt. Aber wenn jemand das Schweigen als allgemeine Praxis misslingt (es sei denn, er arbeitet in einem Bereich, in dem es Sinn macht), möchte ich wirklich nicht an einem Projekt arbeiten, in das er Code einfügt.
Winston Ewert

Antworten:


66

Es kommt auf den 'Vertrag' an:

Wenn PowerManager muss ein gültiges haben IMsgSender, nie für null überprüfen, lassen Sie es früher sterben.

Wenn auf der anderen Seite, es KANN eine haben IMsgSender, dann müssen Sie jedes Mal , wenn Sie überprüfen verwenden, so einfach ist das.

Abschließender Kommentar zur Geschichte des Junior-Programmierers, das Problem ist eigentlich das Fehlen von Testverfahren.


4
+1 für eine sehr prägnante Antwort, die so ziemlich alles zusammenfasst, wie ich es fühle: "es kommt darauf an ..." Sie hatten auch den Mut zu sagen "nie auf null prüfen" ( wenn null ungültig ist). Das Platzieren einer assert()in jeder Member-Funktion, die einen Zeiger dereferenziert, ist ein Kompromiss, der wahrscheinlich viele Leute beschwichtigen wird, aber selbst das fühlt sich für mich wie spekulative Paranoia an. Die Liste der Dinge, die sich meiner Kontrolle entziehen, ist unendlich, und wenn ich mir erst einmal Gedanken darüber mache, wird es zu einem wirklich rutschigen Hang. Wenn ich gelernt habe, meinen Tests, meinen Kollegen und meinem Debugger zu vertrauen, konnte ich nachts besser schlafen.
Evadeflow

2
Wenn Sie Code lesen, können diese Zusicherungen sehr hilfreich sein, um zu verstehen, wie der Code funktionieren soll. Ich habe noch nie eine Codebasis gefunden, die mich dazu brachte, zu sagen: "Ich wünschte, ich hätte nicht all diese Behauptungen überall.", Aber ich habe viel gesehen, wo ich das Gegenteil gesagt habe.
Kristof Provost

1
Schlicht und einfach, das ist die richtige Antwort auf die Frage.
Matthew Azkimov

2
Dies ist die am ehesten zutreffende Antwort, aber ich kann nicht mit "Nie auf Null prüfen" einverstanden sein, sondern immer an dem Punkt auf ungültige Parameter prüfen, an dem sie einer Mitgliedsvariablen zugewiesen werden.
James

Danke für die Kommentare. Es ist besser, es schneller abstürzen zu lassen, wenn jemand die Spezifikation nicht liest und sie mit dem Nullwert initialisiert, wenn dort etwas Gültiges vorhanden sein muss.
Aaronps

77

Ich finde, der Code sollte lauten:

PowerManager::PowerManager(IMsgSender* msgSender)
  : msgSender_(msgSender)
{
    assert(msgSender);
}

void PowerManager::SignalShutdown()
{
    assert(msgSender_);
    msgSender_->sendMsg("shutdown()");
}

Dies ist eigentlich besser als die NULL zu schützen, da es sehr deutlich macht, dass die Funktion niemals aufgerufen werden sollte, wenn sie msgSender_NULL ist. Es stellt auch sicher, dass Sie feststellen, wenn dies passiert.

Die geteilte "Weisheit" Ihrer Kollegen wird diesen Fehler stillschweigend ignorieren und zu unvorhersehbaren Ergebnissen führen.

Im Allgemeinen sind Fehler leichter zu beheben, wenn sie näher an ihrer Ursache erkannt werden. In diesem Beispiel würde der vorgeschlagene NULL-Schutz dazu führen, dass die Meldung zum Herunterfahren nicht festgelegt wird, was möglicherweise zu einem erkennbaren Fehler führt. Es fällt Ihnen schwerer, mit der SignalShutdownFunktion zurückzukehren, als wenn die gesamte Anwendung gerade gestorben wäre und ein handliches Backtrace oder ein Core-Dump erstellt würde, auf das direkt verwiesen wird SignalShutdown().

Es ist ein wenig eingängig, aber ein Absturz, sobald etwas nicht stimmt, macht Ihren Code robuster. Das liegt daran, dass Sie die Probleme tatsächlich finden und tendenziell auch sehr offensichtliche Ursachen haben.


10
Der große Unterschied zwischen dem Aufruf von assert und dem Codebeispiel des OP besteht darin, dass assert nur in Debugbuilds aktiviert wird (#define NDEBUG), wenn das Produkt in die Hände des Kunden gelangt und das Debugging deaktiviert ist und eine nie gestellte Kombination von Codepfaden ausgeführt wird, um zu verlassen Der Zeiger ist null, obwohl die betreffende Funktion ausgeführt wird. Die Zusicherung bietet jedoch keinen Schutz.
2.

17
Vermutlich hätten Sie den Build getestet, bevor Sie ihn tatsächlich an den Kunden gesendet haben, aber ja, dies ist ein mögliches Risiko. Die Lösungen sind jedoch einfach: Entweder nur mit aktivierten Asserts (das ist die Version, die Sie ohnehin getestet haben) oder durch ein eigenes Makro ersetzen, das im Debug-Modus bestätigt und nur Protokolle, Protokolle + Backtraces, Exits ... im Release-Modus erstellt.
Kristof Provost

7
@James - In 90% der Fälle können Sie die fehlgeschlagene NULL-Prüfung ebenfalls nicht beheben. In diesem Fall schlägt der Code jedoch unbemerkt fehl, und der Fehler tritt möglicherweise viel später auf. Sie dachten beispielsweise, dass Sie in einer Datei gespeichert haben, die Nullprüfung schlägt jedoch manchmal fehl, sodass Sie nicht mehr überlegen sind. Sie können sich nicht von jeder Situation erholen, die nicht eintreten darf. Sie können überprüfen, ob dies nicht der Fall ist, aber ich glaube nicht, dass Sie ein Budget haben, es sei denn, Sie schreiben Code für den NASA-Satelliten oder das Atomkraftwerk. Sie müssen eine Art zu sagen haben "Ich habe keine Ahnung, was los ist - es wird nicht angenommen, dass sich das Programm in diesem Zustand befindet".
Maciej Piechotka

6
@ James Ich bin nicht einverstanden mit Werfen. Das lässt es so aussehen, als ob es tatsächlich passieren kann. Behauptungen sind für Dinge, die unmöglich sein sollen. Für echte Bugs im Code also. Ausnahmen gelten für unerwartete, aber mögliche Ereignisse. Ausnahmen sind für Dinge, die man sich vorstellen kann, umzugehen und sich davon zu erholen. Logikfehler in dem Code, den Sie nicht wiederherstellen können, und Sie sollten es auch nicht versuchen.
Kristof Provost

2
@James: Nach meiner Erfahrung mit eingebetteten Systemen sind Software und Hardware immer so konzipiert, dass es extrem schwierig ist, ein Gerät vollständig unbrauchbar zu machen. In den meisten Fällen gibt es einen Hardware-Watchdog, der ein vollständiges Zurücksetzen des Geräts erzwingt, wenn die Software nicht mehr ausgeführt wird.
Bart van Ingen Schenau

30

Wenn msgSender niemals sein darf null, sollten Sie die nullPrüfung nur im Konstruktor ablegen . Dies gilt auch für alle anderen Eingaben in die Klasse - setzen Sie die Integritätsprüfung an der Stelle des Eintritts in das 'Modul' - Klasse, Funktion usw.

Meine Faustregel lautet, Integritätsprüfungen zwischen Modulgrenzen durchzuführen - in diesem Fall der Klasse. Darüber hinaus sollte eine Klasse klein genug sein, um die Integrität der Lebenszeiten der Klassenmitglieder geistig schnell überprüfen zu können. So wird sichergestellt, dass Fehler wie falsche Lösch- / Nullzuweisungen vermieden werden. Die Null-Prüfung, die im Code Ihres Posts durchgeführt wird, setzt voraus, dass eine ungültige Verwendung dem Zeiger tatsächlich Null zuweist - was nicht immer der Fall ist. Da "ungültige Verwendung" impliziert, dass Annahmen über regulären Code nicht zutreffen, können wir nicht sicher sein, alle Arten von Zeigerfehlern abzufangen - zum Beispiel ein ungültiges Löschen, Inkrementieren usw.

Wenn Sie sicher sind, dass das Argument niemals null sein kann, sollten Sie Referenzen verwenden, je nachdem, wie Sie die Klasse verwenden. Verwendung Ansonsten betrachten std::unique_ptroder std::shared_ptranstelle eines Rohzeiger.


11

Nein, es ist nicht sinnvoll, jede Zeiger-Dereferenzierung auf das Zeiger-Sein zu überprüfen NULL.

Nullzeigerprüfungen sind nützlich für Funktionsargumente (einschließlich Konstruktorargumente), um sicherzustellen, dass die Voraussetzungen erfüllt sind, oder um geeignete Maßnahmen zu ergreifen, wenn kein optionaler Parameter angegeben ist, und um die Invariante einer Klasse zu überprüfen, nachdem Sie die Interna der Klasse offengelegt haben. Aber wenn der einzige Grund, warum ein Zeiger NULL geworden ist, das Vorhandensein eines Fehlers ist, hat das Prüfen keinen Sinn. Dieser Fehler hätte genauso gut den Zeiger auf einen anderen ungültigen Wert setzen können.

Wenn ich mit einer Situation wie Ihrer konfrontiert wäre, würde ich zwei Fragen stellen:

  • Warum wurde der Fehler dieses Junior-Programmierers vor der Veröffentlichung nicht entdeckt? Zum Beispiel in einer Codeüberprüfung oder in der Testphase? Ein fehlerhaftes Unterhaltungselektronikgerät auf den Markt zu bringen, kann genauso kostspielig sein (wenn Sie Marktanteile und den guten Willen berücksichtigen) wie die Freigabe eines fehlerhaften Geräts, das in einer sicherheitskritischen Anwendung verwendet wird andere QA-Aktivitäten.
  • Wenn die Null-Prüfung fehlschlägt, welche Fehlerbehandlung soll ich einrichten, oder könnte ich assert(msgSender_)statt der Null-Prüfung einfach schreiben ? Wenn Sie nur den Null-Check-In eingegeben haben, haben Sie möglicherweise einen Absturz verhindert, aber möglicherweise eine schlimmere Situation geschaffen, da die Software unter der Voraussetzung fortfährt, dass ein Vorgang stattgefunden hat, während dieser Vorgang in Wirklichkeit übersprungen wurde. Dies kann dazu führen, dass andere Teile der Software instabil werden.

9

In diesem Beispiel geht es eher um die Objektlebensdauer als darum, ob ein Eingabeparameter null † ist oder nicht. Da Sie erwähnen , dass die PowerManagerMuss immer ein gültiges haben IMsgSender, durch den Zeiger das Argument übergeben ( und damit die Möglichkeit einer Null - Zeiger erlaubt) scheint mir ein Konstruktionsfehler ††.

In solchen Situationen würde ich es vorziehen, die Benutzeroberfläche zu ändern, damit die Anforderungen des Anrufers durch die Sprache durchgesetzt werden:

PowerManager::PowerManager(const IMsgSender& msgSender)
  : msgSender_(msgSender) {}

void PowerManager::SignalShutdown() {
    msgSender_->sendMsg("shutdown()");
}

Wenn Sie es auf diese Weise umschreiben, PowerManagermüssen Sie einen Verweis IMsgSenderfür sein gesamtes Leben aufbewahren. Dies wiederum stellt auch eine implizite Anforderung dar, IMsgSenderdie länger leben muss als PowerManagerund negiert die Notwendigkeit einer Überprüfung oder Behauptung von Nullzeigern PowerManager.

Sie können dasselbe auch mit einem intelligenten Zeiger (über boost oder c ++ 11) schreiben, um explizit zu erzwingen IMsgSender, dass Sie länger leben als PowerManager:

PowerManager::PowerManager(std::shared_ptr<IMsgSender> msgSender) 
  : msgSender_(msgSender) {}

void PowerManager::SignalShutdown() {
    // Here, we own a smart pointer to IMsgSender, so even if the caller
    // destroys the original pointer, we still have a valid copy
    msgSender_->sendMsg("shutdown()");
}

Diese Methode wird bevorzugt, wenn möglicherweise IMsgSendernicht garantiert werden kann, dass die Lebensdauer länger ist als PowerManager(dh x = new IMsgSender(); p = new PowerManager(*x);).

† In Bezug auf Zeiger: Die zügellose Nullprüfung erschwert das Lesen von Code und verbessert die Stabilität nicht (sie verbessert das Erscheinungsbild der Stabilität, was viel schlimmer ist).

Irgendwo hat jemand eine Adresse zur Erinnerung bekommen, an der die IMsgSender. Es liegt in der Verantwortung dieser Funktion, sicherzustellen, dass die Zuordnung erfolgreich war (Überprüfung der Rückgabewerte der Bibliothek oder ordnungsgemäße Behandlung von std::bad_allocAusnahmen), um ungültige Zeiger nicht zu umgehen.

Da das PowerManagernicht der Besitzer ist IMsgSender(es leiht es sich nur für eine Weile aus), ist es nicht dafür verantwortlich, diesen Speicher zuzuweisen oder zu zerstören. Dies ist ein weiterer Grund, warum ich die Referenz bevorzuge.

†† Da Sie neu in diesem Job sind, gehe ich davon aus, dass Sie vorhandenen Code hacken. Mit Konstruktionsfehler meine ich, dass der Fehler in dem Code liegt, mit dem Sie arbeiten. Die Leute, die Ihren Code kennzeichnen, weil er nicht auf Nullzeiger prüft, kennzeichnen sich selbst, um Code zu schreiben, der Zeiger erfordert :)


1
Es ist eigentlich nichts Falsches daran, manchmal rohe Zeiger zu verwenden. Std :: shared_ptr ist kein Wundermittel, und die Verwendung in einer Argumentationsliste ist ein Heilmittel gegen schlampiges Engineering, obwohl ich feststelle, dass es als "leet" gilt, es zu verwenden, wann immer es möglich ist. Verwenden Sie Java, wenn Sie das möchten.
James

2
Ich habe nicht gesagt, dass rohe Zeiger schlecht sind! Sie haben definitiv ihren Platz. Dies ist jedoch C + + , Referenzen (und gemeinsame Zeiger, jetzt) sind Teil der Sprache für einen Grund. Verwenden Sie sie, sie werden Sie zum Erfolg führen.
Seth

Ich mag die Idee des intelligenten Zeigers, aber es ist keine Option in diesem speziellen Gig. +1 für die Empfehlung der Verwendung von Referenzen (wie bei @Max und einigen anderen). Manchmal ist es jedoch aufgrund von Henne-Ei-Abhängigkeitsproblemen nicht möglich, eine Referenz zu injizieren, dh, wenn in ein Objekt, das ansonsten ein Kandidat für die Injektion wäre, ein Zeiger (oder eine Referenz) auf seinen Halter injiziert werden muss . Dies kann manchmal auf eine eng gekoppelte Konstruktion hinweisen. aber es ist oft akzeptabel, wie in der Beziehung zwischen einem Objekt und seinem Iterator, wo es niemals Sinn macht, das letztere ohne das erstere zu verwenden.
Evadeflow

8

Wie bei Ausnahmen sind Schutzbedingungen nur dann nützlich, wenn Sie wissen, wie Sie den Fehler beheben können, oder wenn Sie eine aussagekräftigere Ausnahmemeldung anzeigen möchten.

Das Verschlucken eines Fehlers (ob als Ausnahme oder als Guard-Check) ist nur dann richtig, wenn der Fehler keine Rolle spielt. Die häufigste Ursache für das Verschlucken von Fehlern ist der Fehlerprotokollierungscode. Sie möchten keine App zum Absturz bringen, da Sie keine Statusmeldung protokollieren konnten.

Jedes Mal, wenn eine Funktion aufgerufen wird und dies kein optionales Verhalten ist, sollte sie laut und nicht lautlos fehlschlagen.

Bearbeiten: Beim Nachdenken über Ihre Junior-Programmierer-Geschichte klingt es so, als ob ein privates Mitglied auf null gesetzt wurde, wenn dies niemals zugelassen werden sollte. Sie hatten ein Problem mit unangemessenem Schreiben und versuchen, es durch Überprüfen beim Lesen zu beheben. Das ist rückwärts. Zum Zeitpunkt der Identifizierung ist der Fehler bereits aufgetreten. Die Lösung, die Code-Review / Compiler durchsetzen kann, ist keine Schutzbedingung, sondern Get-and-Setter oder Const-Mitglieder.


7

Wie andere angemerkt haben, hängt dies davon ab, ob msgSenderdies legitim sein kann oder nicht NULL. Das Folgende geht davon aus, dass es niemals NULL sein sollte.

void PowerManager::SignalShutdown()
{
    if (!msgSender_)
    {
       throw SignalException("Shut down failed because message sender is not set.");
    }

    msgSender_->sendMsg("shutdown()");
}

Die vorgeschlagene "Korrektur" durch die anderen in Ihrem Team verstößt gegen das Prinzip " Dead Programs Tell No Lies ". Bugs sind so schwer zu finden. Eine Methode, die ihr Verhalten aufgrund eines früheren Problems stillschweigend ändert, erschwert nicht nur das Auffinden des ersten Fehlers, sondern fügt auch einen eigenen zweiten Fehler hinzu.

Der Junior hat Chaos angerichtet, als er nicht nach einer Null gesucht hat. Was ist, wenn dieser Teil des Codes Schaden anrichtet, wenn er weiterhin in einem undefinierten Zustand ausgeführt wird (Gerät ist eingeschaltet, aber das Programm "denkt", dass es ausgeschaltet ist)? Vielleicht tut ein anderer Teil des Programms etwas, das nur sicher ist, wenn das Gerät ausgeschaltet ist.

Mit beiden Ansätzen werden stille Fehler vermieden:

  1. Verwenden Sie die in dieser Antwort vorgeschlagenen Zusicherungen , stellen Sie jedoch sicher, dass sie im Produktionscode aktiviert sind. Dies könnte natürlich zu Problemen führen, wenn andere Zusicherungen mit der Annahme verfasst würden, dass sie in der Produktion ausfallen würden.

  2. Eine Ausnahme auslösen, wenn sie null ist.


5

Ich bin damit einverstanden, die Null im Konstruktor abzufangen. Wenn das Mitglied in der Kopfzeile als deklariert ist:

IMsgSender* const msgSender_;

Dann kann der Zeiger nach der Initialisierung nicht mehr geändert werden. War er also beim Erstellen in Ordnung, ist er für die Lebensdauer des Objekts in Ordnung, das ihn enthält. (Das Objekt gerichtet zu werden nicht const sein.)


Das ist ein toller Rat, danke! So gut, dass es mir leid tut, dass ich nicht höher stimmen kann. Es ist keine direkte Antwort auf die angegebene Frage, aber es ist eine großartige Möglichkeit, die Möglichkeit auszuschließen, dass der Zeiger "versehentlich" wird NULL.
Evadeflow

@evadeflow Ich dachte, "Ich stimme allen anderen zu", beantwortete den Hauptschwerpunkt der Frage. Prost aber! ;-)
Grimm The Opiner

Es wäre ein wenig unhöflich von mir, die akzeptierte Antwort an dieser Stelle zu ändern, wenn man bedenkt, wie die Frage formuliert wurde. Aber ich finde diesen Rat wirklich hervorragend, und es tut mir leid, dass ich selbst nicht daran gedacht habe. Mit einem constZeiger muss man sich nicht assert()außerhalb des Konstruktors befinden, daher scheint diese Antwort sogar ein bisschen besser zu sein als die von @Kristof Provost (die ebenfalls hervorragend war, aber die Frage auch etwas indirekt ansprach). Ich hoffe, dass andere dies stimmen werden, da dies der Fall ist Es macht wirklich nicht assertin jeder Methode Sinn, wenn Sie nur den Zeiger machen können const.
Evadeflow

@evadeflow Die Leute besuchen nur Questins für den Vertreter, jetzt wurde geantwortet, niemand wird es noch einmal anschauen. ;-) Ich bin nur vorbeigekommen, weil es um Zeiger ging und ich ein Auge auf die blutige Brigade "Nutze intelligente Zeiger für alles" habe.
Grimm The Opiner

3

Das ist absolut gefährlich!

Ich habe unter einem leitenden Entwickler in einer C-Codebasis mit den schäbigsten "Standards" gearbeitet, die sich für dasselbe einsetzten, um blind alle Zeiger auf Null zu prüfen. Der Entwickler würde am Ende Folgendes tun:

// Pre: vertex should never be null.
void transform_vertex(Vertex* vertex, ...)
{
    // Inserted by my "wise" co-worker.
    if (!vertex)
        return;
    ...
}

Ich habe einmal versucht, eine solche Überprüfung der Vorbedingung in einer solchen Funktion zu entfernen und durch eine assertzu ersetzen, um zu sehen, was passieren würde.

Zu meinem Entsetzen habe ich Tausende von Codezeilen in der Codebasis gefunden, die Nullen an diese Funktion übergeben haben, aber wo die Entwickler, wahrscheinlich verwirrt, herumgearbeitet und einfach mehr Code hinzugefügt haben, bis die Dinge geklappt haben.

Zu meinem weiteren Entsetzen stellte ich fest, dass dieses Problem an allen möglichen Stellen in der Codebasis auftrat und nach Nullen suchte. Die Codebasis war über Jahrzehnte gewachsen, um sich auf diese Überprüfungen zu verlassen, um im Stillen selbst die am deutlichsten dokumentierten Voraussetzungen zu verletzen. Durch das Entfernen dieser tödlichen Schecks zu Gunsten von würden assertsalle logischen menschlichen Fehler über Jahrzehnte in der Codebasis aufgedeckt, und wir würden darin ertrinken.

Es dauerte nur zwei scheinbar unschuldige Codezeilen wie diese + Zeit und ein Team, um am Ende tausend angesammelte Bugs zu maskieren.

Dies sind die Arten von Methoden, die dazu führen, dass Fehler von anderen Fehlern abhängen, damit die Software funktioniert. Es ist ein Alptraumszenario . Jeder logische Fehler im Zusammenhang mit der Verletzung derartiger Voraussetzungen wird auf mysteriöse Weise in einer Entfernung von einer Million Codezeilen von der tatsächlichen Stelle angezeigt, an der der Fehler aufgetreten ist, da alle diese Nullprüfungen nur den Fehler verbergen und den Fehler verbergen, bis wir einen Ort erreichen, der vergessen hat um den Fehler zu verbergen.

Das blinde Überprüfen auf Nullen an jeder Stelle, an der ein Nullzeiger eine Vorbedingung verletzt, ist für mich völliger Wahnsinn, es sei denn, Ihre Software ist für Durchsetzungsfehler und Produktionsabstürze so geschäftskritisch, dass das Potenzial dieses Szenarios vorzuziehen ist.

Ist es für einen Codierungsstandard sinnvoll, zu verlangen, dass jeder einzelne in einer Funktion dereferenzierte Zeiger zuerst auf NULL überprüft wird - sogar auf private Datenelemente?

Also würde ich sagen, absolut nicht. Es ist nicht einmal "sicher". Es kann durchaus das Gegenteil sein und alle Arten von Fehlern in Ihrer Codebasis verbergen, die im Laufe der Jahre zu den schrecklichsten Szenarien führen können.

assertist der Weg hierher. Verstöße gegen die Voraussetzungen dürfen nicht unbemerkt bleiben, sonst kann das Gesetz von Murphy leicht greifen.


1
"assert" ist der richtige Weg - aber denken Sie daran, dass in jedem modernen System jeder Speicherzugriff über einen Zeiger eine in die Hardware integrierte Nullzeiger-Assert-Funktion hat. Also in "assert (p! = NULL); return * p;" ist selbst die Aussage meistens sinnlos.
gnasher729

@ gnasher729 Ah, das ist ein sehr guter Punkt, aber ich neige dazu, assertsowohl einen Dokumentationsmechanismus zur Ermittlung der Voraussetzungen als auch eine Möglichkeit zu sehen, einen Abbruch zu erzwingen. Aber ich mag auch die Art des Assertionsfehlers, die genau anzeigt, welche Codezeile einen Fehler verursacht hat und die Assertionsbedingung fehlgeschlagen ist ("Dokumentation des Absturzes"). Kann sich als nützlich erweisen, wenn wir einen Debugbuild außerhalb eines Debuggers verwenden und unvorbereitet sind.

@ gnasher729 Oder ich habe vielleicht einen Teil davon falsch verstanden. Zeigen diese modernen Systeme, in welcher Quellcodezeile die Behauptung fehlgeschlagen ist? Ich könnte mir vorstellen, dass sie zu diesem Zeitpunkt die Informationen verloren haben und möglicherweise nur eine Verletzung durch Segfault / Zugriff anzeigen, aber ich bin weit davon entfernt, "modern" zu sein - für den größten Teil meiner Entwicklung unter Windows 7 immer noch hartnäckig.

Wenn Ihre App beispielsweise unter MacOS X und iOS aufgrund eines Nullzeigerzugriffs während der Entwicklung abstürzt, teilt Ihnen der Debugger die genaue Codezeile mit, in der sie vorkommt. Es gibt noch einen anderen merkwürdigen Aspekt: ​​Der Compiler wird versuchen, Sie zu warnen, wenn Sie etwas Verdächtiges tun. Wenn Sie einen Zeiger auf eine Funktion übergeben und auf diese zugreifen, gibt der Compiler keine Warnung aus, dass der Zeiger möglicherweise NULL ist, da dies so häufig vorkommt, dass Sie in Warnungen ertrinken würden. Wenn Sie jedoch einen Vergleich tun , wenn (p == NULL) ... dann geht der Compiler davon , dass es eine gute Chance, dass p ist NULL,
gnasher729

denn warum hättest du es anders getestet, daher gibt es von nun an eine Warnung, wenn du es ohne Prüfung verwendest. Wenn Sie ein eigenes assert-ähnliches Makro verwenden, müssen Sie ein bisschen clever sein, um es so zu schreiben, dass "my_assert (p! = NULL," Es ist ein Nullzeiger, dumm! "); * P = 1;" gibt dir keine Warnung.
gnasher729

2

In Objective-C wird beispielsweise jeder Methodenaufruf für ein nilObjekt als No-Op behandelt, das zu einem Null-Wert ausgewertet wird. Diese Entwurfsentscheidung in Objective-C bietet aus den in Ihrer Frage angegebenen Gründen einige Vorteile. Das theoretische Konzept des Null-Guarding jedes Methodenaufrufs hat einige Vorteile, wenn es gut bekannt gemacht und konsequent angewendet wird.

Das heißt, Code lebt in einem Ökosystem, nicht in einem Vakuum. Das null-geschützte Verhalten wäre in C ++ nicht idiomatisch und überraschend und sollte daher als schädlich angesehen werden. Zusammenfassend kann gesagt werden, dass niemand C ++ - Code auf diese Weise schreibt. Tun Sie es also nicht ! Als Gegenbeispiel, beachten Sie jedoch, dass anrufen free()oder deleteauf ein NULLin C und C ++ ist garantiert ein no-op zu sein.


In Ihrem Beispiel wäre es wahrscheinlich sinnvoll, eine Behauptung im Konstruktor abzulegen, die msgSendernicht null ist. Wenn der Konstruktor eine Methode namens auf msgSendersofort, dann keine solche Behauptung wäre notwendig, da es genau dort sowieso abstürzen würde. Da es jedoch nur für die zukünftige Verwendung gespeichert msgSender wird, ist es nicht offensichtlich, SignalShutdown()wie der Wert im Stack ermittelt wurde NULL, sodass eine Zusicherung im Konstruktor das Debuggen erheblich vereinfachen würde.

Noch besser, der Konstruktor sollte eine const IMsgSender&Referenz akzeptieren , die unmöglich sein kann NULL.


1

Der Grund, warum Sie aufgefordert werden, Null-Dereferenzen zu vermeiden, besteht darin, sicherzustellen, dass Ihr Code robust ist. Beispiele für Junior-Programmierer vor langer Zeit sind nur Beispiele. Jeder kann den Code versehentlich brechen und eine Null-Dereferenzierung verursachen - insbesondere für globale und Klassen-Globale. In C und C ++ ist dies mit der direkten Speicherverwaltung sogar noch versehentlicher möglich. Sie werden vielleicht überrascht sein, aber so etwas passiert sehr oft. Selbst von sehr sachkundigen, sehr erfahrenen und sehr erfahrenen Entwicklern.

Sie müssen nicht alles auf Null setzen, aber Sie müssen sich vor Dereferenzen schützen, die mit ziemlicher Wahrscheinlichkeit auf Null gesetzt sind. Dies ist häufig der Fall, wenn sie in verschiedenen Funktionen zugewiesen, verwendet und dereferenziert werden. Es ist möglich, dass eine der anderen Funktionen geändert wird und Ihre Funktion beeinträchtigt. Es ist auch möglich, dass eine der anderen Funktionen nicht in der richtigen Reihenfolge aufgerufen wird (z. B. wenn Sie einen Deallocator haben, der separat vom Destruktor aufgerufen werden kann).

Ich bevorzuge den Ansatz, den Ihre Mitarbeiter Ihnen in Kombination mit der Verwendung von Assert mitteilen. In der Testumgebung kommt es zu einem Absturz, sodass es offensichtlicher ist, dass es ein Problem gibt, das in der Produktion behoben werden muss und das ordnungsgemäß fehlschlägt.

Sie sollten auch ein robustes Code-Korrektheitstool wie Coverity oder Fortify verwenden. Und Sie sollten alle Compiler-Warnungen berücksichtigen.

Bearbeiten: Wie andere bereits erwähnt haben, ist es im Allgemeinen auch falsch, stillschweigend zu versagen, wie in mehreren Codebeispielen. Wenn Ihre Funktion den Wert Null nicht wiederherstellen kann, sollte sie einen Fehler (oder eine Ausnahme) an ihren Aufrufer zurückgeben. Der Anrufer ist dafür verantwortlich, dass er entweder seine Anrufreihenfolge festlegt, einen Fehler behebt oder einen Fehler zurückgibt (oder eine Ausnahme auslöst) und so weiter. Schließlich kann eine Funktion ordnungsgemäß wiederherstellen und fortfahren, ordnungsgemäß wiederherstellen und fehlschlagen (z. B. eine Datenbank, bei der eine Transaktion aufgrund eines internen Fehlers für einen Benutzer fehlschlägt, der jedoch nicht ordnungsgemäß beendet wird), oder die Funktion stellt fest, dass der Anwendungsstatus beschädigt ist und nicht wiederherstellbar und die Anwendung wird beendet.


+1 für den Mut, eine (scheinbar) unpopuläre Position zu vertreten. Ich wäre enttäuscht gewesen, wenn niemand meine Kollegen in dieser Angelegenheit verteidigt hätte (es sind sehr kluge Leute, die seit vielen Jahren ein Qualitätsprodukt herausbringen). Mir gefällt auch die Idee, dass Apps "in der Testumgebung abstürzen ... und in der Produktion ordnungsgemäß scheitern" sollten. Ich bin nicht davon überzeugt, dass das Mandatieren von "roten" NULLSchecks der Weg ist, aber ich freue mich über eine Stellungnahme von jemandem außerhalb meines Arbeitsplatzes, der im Grunde sagt: "Aigh. Scheint nicht zu unvernünftig."
Evadeflow

Ich ärgere mich, wenn Leute nach malloc () auf NULL testen. Es sagt mir, dass sie nicht verstehen, wie moderne Betriebssysteme Speicher verwalten.
Kristof Provost

2
Ich vermute, dass es sich bei @KristofProvost um Betriebssysteme handelt, die Overcommit verwenden. Dies gelingt malloc immer (aber das Betriebssystem kann später Ihren Prozess beenden, wenn nicht genügend Arbeitsspeicher verfügbar ist). Dies ist jedoch kein guter Grund, Nullprüfungen auf malloc zu überspringen: overcommit ist nicht universell für alle Plattformen. Auch wenn es aktiviert ist, kann es andere Gründe für das Fehlschlagen von malloc geben (z. B. unter Linux ein mit ulimit festgelegtes Prozessadressraumlimit) ).
John Bartholomew

1
Was John gesagt hat. Ich habe in der Tat von Überbindung gesprochen. Overcommit ist unter Linux standardmäßig aktiviert (was meine Rechnungen bezahlt). Ich weiß es nicht, aber ich vermute es, wenn es unter Windows dasselbe ist. In den meisten Fällen werden Sie sowieso abstürzen, es sei denn, Sie sind bereit, eine Menge Code zu schreiben , der niemals getestet wird, um Fehler zu behandeln, die mit ziemlicher Sicherheit niemals auftreten werden ...
Kristof Provost

2
Lassen Sie mich hinzufügen, dass ich auch noch nie einen Code gesehen habe (außerhalb des Linux-Kernels, bei dem Speichermangel realistischerweise möglich ist), der tatsächlich eine Speichermangelbedingung richtig handhabt. Der gesamte Code, den ich gesehen habe, würde sowieso später abstürzen. Alles, was erreicht wurde, war, Zeit zu verschwenden, den Code schwerer verständlich zu machen und das eigentliche Problem zu verbergen. Null-Dereferenzen sind einfach zu debuggen (wenn Sie eine Kerndatei haben).
Kristof Provost

1

Die Verwendung eines Zeiger anstelle einer Referenz würde mir sagen , dass msgSenderist nur eine Option , und hier ist der Null - Check würde richtig sein. Das Code-Snippet ist zu langsam, um das zu entscheiden. Vielleicht gibt es noch andere Elemente PowerManager, die wertvoll (oder überprüfbar) sind ...

Bei der Wahl zwischen Zeiger und Referenz wäge ich beide Optionen sorgfältig ab. Wenn ich einen Zeiger für ein Mitglied verwenden muss (auch für private Mitglieder), muss ich die if (x)Prozedur jedes Mal akzeptieren , wenn ich sie dereferenziere.


Wie ich irgendwo in einem anderen Kommentar erwähnt habe, gibt es Zeiten, in denen das Einfügen einer Referenz nicht möglich ist. Ein Beispiel, auf das ich häufig
stoße,

@evadeflow: OK, ich gebe zu, es hängt von der Klassengröße und der Sichtbarkeit der Zeigerelemente (die nicht referenziert werden können) ab, ob ich sie vor dem Dereferenzieren überprüfe. In Fällen, in denen die Klasse klein und einfach ist und die Zeiger privat sind, kann ich nicht ...
Wolf

0

Es kommt darauf an, was passieren soll.

Sie haben geschrieben, dass Sie an "einem Unterhaltungselektronikgerät" arbeiten. Wenn irgendwie ein Fehler durch Setzen von msgSender_auf eingeführt wirdNULL möchten Sie dies tun

  • das Gerät weitermachen, überspringen SignalShutdown aber den Rest seines Betriebs fortsetzt, oder
  • das Gerät zum Absturz bringen und den Benutzer zum Neustart zwingen?

Abhängig von der Auswirkung des nicht gesendeten Abschaltsignals ist Option 1 möglicherweise eine sinnvolle Wahl. Wenn der Benutzer weiterhin Musik hören kann, das Display jedoch weiterhin den Titel des vorherigen Titels anzeigt, ist dies möglicherweise einem vollständigen Absturz des Geräts vorzuziehen.

Wenn Sie Option 1 wählen, ist natürlich eine assert(wie von anderen empfohlen) von entscheidender Bedeutung , um die Wahrscheinlichkeit zu verringern, dass ein solcher Fehler während der Entwicklung unbemerkt auftritt. Der ifNullwächter ist nur für die Ausfallminderung in der Produktion gedacht.

Persönlich bevorzuge ich auch den "Crash Early" -Ansatz für Produktions-Builds, aber ich entwickle gerade eine Business-Software, die im Falle eines Fehlers einfach repariert und aktualisiert werden kann. Bei Geräten der Unterhaltungselektronik ist dies möglicherweise nicht so einfach.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.