Ist die Verwendung von assert () in C ++ eine schlechte Praxis?

93

Ich neige dazu, meinem C ++ - Code viele Zusicherungen hinzuzufügen, um das Debuggen zu vereinfachen, ohne die Leistung von Release-Builds zu beeinträchtigen. Jetzt asserthandelt es sich um ein reines C-Makro, das ohne Berücksichtigung von C ++ - Mechanismen entwickelt wurde.

C ++ definiert dagegen std::logic_error, was in Fällen ausgelöst werden soll, in denen ein Fehler in der Programmlogik vorliegt (daher der Name). Das Auslösen einer Instanz ist möglicherweise die perfekte Alternative zu C ++ assert.

Das Problem ist , dass assertund abortsowohl das Programm sofort beenden , ohne Destruktoren aufrufen, damit die Bereinigung übersprungen, während eine Ausnahme werfen manuell fügt unnötige Laufzeitkosten. Eine Möglichkeit, dies zu umgehen, besteht darin, ein eigenes Assertionsmakro zu erstellen SAFE_ASSERT, das genau wie das C-Gegenstück funktioniert, jedoch bei einem Fehler eine Ausnahme auslöst.

Ich kann mir drei Meinungen zu diesem Problem vorstellen:

  • Halten Sie sich an Cs Behauptung. Da das Programm sofort beendet wird, spielt es keine Rolle, ob Änderungen korrekt abgewickelt werden. Ebenso #defineschlecht ist die Verwendung von s in C ++.
  • Wirf eine Ausnahme und fange sie in main () ab . Das Überspringen von Destruktoren durch Code in einem beliebigen Status des Programms ist eine schlechte Praxis und muss unter allen Umständen vermieden werden, ebenso wie Aufrufe zum Beenden (). Wenn Ausnahmen ausgelöst werden, müssen sie abgefangen werden.
  • Wirf eine Ausnahme und lass sie das Programm beenden. Eine Ausnahme beim Beenden eines Programms ist in Ordnung, und aufgrund NDEBUGdessen wird dies in einem Release-Build niemals passieren. Das Abfangen ist nicht erforderlich und macht Implementierungsdetails des internen Codes verfügbar main().

Gibt es eine endgültige Antwort auf dieses Problem? Irgendeine professionelle Referenz?

Bearbeitet: Das Überspringen von Destruktoren ist natürlich kein undefiniertes Verhalten.

Fabian Knorr
quelle
22
Nein, wirklich, logic_errorist der logische Fehler. Ein Fehler in der Programmlogik wird als Fehler bezeichnet. Sie lösen Fehler nicht, indem Sie Ausnahmen auslösen.
R. Martinho Fernandes
4
Behauptungen, Ausnahmen, Fehlercodes. Jeder hat einen völlig anderen Anwendungsfall, und Sie sollten keinen verwenden, wenn ein anderer benötigt wird.
Kerrek SB
5
Stellen Sie sicher, dass Sie dort verwenden, static_assertwo es angebracht ist, wenn Sie dies zur Verfügung haben.
Flexo
4
@trion Ich sehe nicht, wie das hilft. Würdest du werfen std::bug?
R. Martinho Fernandes
3
@trion: Tu das nicht. Ausnahmen gelten nicht für das Debuggen. Jemand könnte die Ausnahme abfangen. Sie müssen sich beim Telefonieren keine Sorgen um UB machen std::abort(). Es wird nur ein Signal ausgelöst, das den Prozess beendet.
Kerrek SB

Antworten:

73

Behauptungen sind im C ++ - Code völlig angemessen. Ausnahmen und andere Fehlerbehandlungsmechanismen sind nicht wirklich für das Gleiche wie Behauptungen gedacht.

Die Fehlerbehandlung ist für den Fall vorgesehen, dass ein Fehler möglicherweise behoben oder dem Benutzer ordnungsgemäß gemeldet werden kann. Wenn beispielsweise beim Lesen einer Eingabedatei ein Fehler auftritt, möchten Sie möglicherweise etwas dagegen tun. Fehler können aus Fehlern resultieren, aber sie können auch einfach die geeignete Ausgabe für eine bestimmte Eingabe sein.

Behauptungen beziehen sich beispielsweise auf die Überprüfung, ob die Anforderungen einer API erfüllt sind, wenn die API normalerweise nicht überprüft wird, oder auf die Überprüfung von Dingen, von denen der Entwickler glaubt, dass sie durch die Konstruktion garantiert sind. Wenn ein Algorithmus beispielsweise sortierte Eingaben erfordert, würden Sie dies normalerweise nicht überprüfen, aber Sie haben möglicherweise die Zusicherung, dies zu überprüfen, damit Debug-Builds diese Art von Fehler kennzeichnen. Eine Zusicherung sollte immer auf ein falsch funktionierendes Programm hinweisen.


Wenn Sie ein Programm schreiben, bei dem ein unsauberes Herunterfahren ein Problem verursachen kann, sollten Sie Behauptungen vermeiden. Undefiniertes Verhalten, das ausschließlich der C ++ - Sprache entspricht, ist hier kein solches Problem, da das Treffen einer Behauptung wahrscheinlich bereits das Ergebnis eines undefinierten Verhaltens oder der Verletzung einer anderen Anforderung ist, die verhindern könnte, dass eine Bereinigung ordnungsgemäß funktioniert.

Auch wenn Sie Behauptungen in Bezug auf eine Ausnahme implementieren, kann diese möglicherweise abgefangen und "behandelt" werden, obwohl dies dem eigentlichen Zweck der Behauptung widerspricht.

bames53
quelle
1
Ich bin mir nicht ganz sicher, ob dies in der Antwort ausdrücklich angegeben wurde, daher sage ich es hier: Sie sollten keine Behauptung für irgendetwas verwenden, das Benutzereingaben beinhaltet, die zum Zeitpunkt des Schreibens des Codes nicht bestimmt werden können. Wenn ein Benutzer 3anstelle 1Ihres Codes übergibt , sollte er im Allgemeinen keine Zusicherung auslösen. Behauptungen sind nur Programmiererfehler, keine Benutzer der Bibliothek oder Anwendungsfehler.
SS Anne
101
  • Zusicherungen dienen zum Debuggen . Der Benutzer Ihres Versandcodes sollte diese niemals sehen. Wenn eine Zusicherung getroffen wird, muss Ihr Code korrigiert werden.

  • Ausnahmen gelten für außergewöhnliche Umstände . Wenn einer angetroffen wird, kann der Benutzer nicht tun, was er will, kann aber möglicherweise an einer anderen Stelle fortfahren.

  • Die Fehlerbehandlung gilt für den normalen Programmablauf. Wenn Sie beispielsweise den Benutzer zur Eingabe einer Nummer auffordern und etwas Unparsables erhalten, ist dies normal , da Benutzereingaben nicht unter Ihrer Kontrolle stehen und Sie selbstverständlich immer mit allen möglichen Situationen umgehen müssen. (ZB Schleife, bis Sie eine gültige Eingabe haben und dazwischen "Entschuldigung, versuchen Sie es erneut" sagen.)

Kerrek SB
quelle
1
kam auf der Suche nach dieser Behauptung; Jede Form der Behauptung, die an den Produktionscode weitergegeben wird, weist auf schlechtes Design und schlechte Qualitätssicherung hin. Der Punkt, an dem eine Zusicherung aufgerufen wird, ist der Punkt, an dem eine fehlerhafte Behandlung einer Fehlerbedingung erfolgen sollte. (Ich benutze niemals Assert's). Was Ausnahmen betrifft , ist der einzige mir bekannte Anwendungsfall, wenn ctor möglicherweise fehlschlägt, alle anderen dienen der normalen Fehlerbehandlung.
Slashmais
5
@slashmais: Das Gefühl ist lobenswert, aber wenn Sie keinen perfekten, fehlerfreien Code liefern, finde ich eine Behauptung (sogar eine, die den Benutzer zum Absturz bringt) dem undefinierten Verhalten vorzuziehen. Fehler treten in komplexen Systemen auf, und mit einer Behauptung haben Sie die Möglichkeit, sie dort zu sehen und zu diagnostizieren, wo sie auftreten.
Kerrek SB
@ KerrekSB Ich würde es vorziehen, eine Ausnahme gegenüber einer Behauptung zu verwenden. Zumindest hat der Code die Möglichkeit, den fehlerhaften Zweig zu verwerfen und etwas anderes Nützliches zu tun. Wenn Sie RAII verwenden, werden zumindest alle Puffer zum Öffnen von Dateien ordnungsgemäß geleert.
Dämonenfrühling
14

Assertions können verwendet werden, um interne Implementierungsinvarianten zu überprüfen, z. B. den internen Status vor oder nach der Ausführung einer Methode usw. Wenn Assertion fehlschlägt, bedeutet dies, dass die Logik des Programms fehlerhaft ist und Sie sich nicht davon erholen können. In diesem Fall ist das Beste, was Sie tun können, so schnell wie möglich zu brechen, ohne eine Ausnahme an den Benutzer zu übergeben. Was an Assertions (zumindest unter Linux) wirklich gut ist, ist, dass der Core-Dump als Ergebnis der Prozessbeendigung generiert wird und Sie so den Stack-Trace und die Variablen leicht untersuchen können. Dies ist viel nützlicher, um den Logikfehler zu verstehen, als eine Ausnahmemeldung.

Nogard
quelle
Ich habe einen ähnlichen Ansatz. Ich verwende Aussagen für die Logik, die wahrscheinlich lokal korrekt sein sollten (z. B. Schleifeninvarianten). Ausnahmen sind Fälle, in denen dem Code durch eine nichtlokale (externe) Situation ein logischer Fehler aufgezwungen wurde .
Spraff
Wenn eine Zusicherung fehlschlägt, bedeutet dies, dass die Logik eines Teils des Programms fehlerhaft ist. Eine fehlgeschlagene Behauptung bedeutet nicht unbedingt, dass nichts erreicht werden kann. Ein kaputtes Plugin sollte wahrscheinlich nicht ein ganzes Textverarbeitungsprogramm abbrechen.
Dämonenfrühling
13

Das Ausführen von Destruktoren aufgrund von alling abort () ist kein undefiniertes Verhalten!

Wenn es so wäre, wäre es ein undefiniertes Verhalten, auch anzurufen std::terminate(), und was wäre der Sinn, es bereitzustellen?

assert() ist in C ++ genauso nützlich wie in C. Behauptungen dienen nicht der Fehlerbehandlung, sondern dem sofortigen Abbruch des Programms.

Jonathan Wakely
quelle
1
Ich würde sagen, abort()ist für den sofortigen Abbruch des Programms. Sie haben Recht, dass Zusicherungen nicht für die Fehlerbehandlung bestimmt sind. Assert versucht jedoch, den Fehler durch Abbrechen zu behandeln. Sollten Sie nicht stattdessen eine Ausnahme auslösen und den Anrufer den Fehler behandeln lassen, wenn dies möglich ist? Schließlich kann der Anrufer besser feststellen, ob es sich aufgrund des Ausfalls einer Funktion nicht lohnt, etwas anderes zu tun. Vielleicht versucht der Anrufer, drei Dinge zu tun, die nichts miteinander zu tun haben, und könnte trotzdem die beiden anderen Jobs erledigen und diesen einfach verwerfen.
Dämonenfrühling
Und assertist zum Aufrufen definiert abort(wenn die Bedingung falsch ist). Was das Auslösen von Ausnahmen betrifft, nein, das ist nicht immer angemessen. Einige Dinge können vom Anrufer nicht erledigt werden. Der Aufrufer kann nicht feststellen, ob ein logischer Fehler in einer Bibliotheksfunktion eines Drittanbieters behoben werden kann oder ob beschädigte Daten behoben werden können.
Jonathan Wakely
6

IMHO, Behauptungen sind für die Überprüfung von Bedingungen, die, wenn verletzt, alles andere Unsinn machen. Und deshalb können Sie sich nicht von ihnen erholen, oder besser gesagt, die Wiederherstellung ist irrelevant.

Ich würde sie in 2 Kategorien einteilen:

  • Entwicklersünden (z. B. eine Wahrscheinlichkeitsfunktion, die negative Werte zurückgibt):

float Wahrscheinlichkeit () {return -1,0; }}

assert (Wahrscheinlichkeit ()> = 0,0)

  • Die Maschine ist defekt (z. B. die Maschine, auf der Ihr Programm ausgeführt wird, ist sehr falsch):

int x = 1;

assert (x> 0);

Dies sind beides triviale Beispiele, aber nicht zu weit von der Realität entfernt. Denken Sie beispielsweise an naive Algorithmen, die negative Indizes für die Verwendung mit Vektoren zurückgeben. Oder eingebettete Programme in benutzerdefinierte Hardware. Oder eher, weil es nicht passiert .

Und wenn es solche Entwicklungsfehler gibt, sollten Sie sich nicht sicher sein, ob ein Wiederherstellungs- oder Fehlerbehandlungsmechanismus implementiert ist. Gleiches gilt für Hardwarefehler.

FranMowinckel
quelle
1
assert (Wahrscheinlichkeit ()> = 0,0)
Elliott