In verschiedenen Disziplinen der Softwareentwicklung gibt es viele Philosophien darüber, wie Bibliotheken mit Fehlern oder anderen außergewöhnlichen Bedingungen umgehen sollen. Einige von denen, die ich gesehen habe:
- Gibt einen Fehlercode mit dem Ergebnis zurück, das von einem Zeigerargument zurückgegeben wird. Dies ist, was PETSc tut.
- Rückgabe von Fehlern durch einen Sentinel-Wert. Zum Beispiel gibt malloc NULL zurück, wenn es keinen Speicher zuordnen konnte,
sqrt
gibt NaN zurück, wenn Sie eine negative Zahl übergeben usw. Dieser Ansatz wird in vielen libc-Funktionen verwendet. - Ausnahmen auslösen. Wird in Deal.II, Trilinos usw. verwendet.
- Geben Sie einen Variantentyp zurück. Beispiel: Eine C ++ - Funktion, die ein Objekt vom Typ zurückgibt,
Result
wenn es ordnungsgemäß ausgeführt wird, und einen Typ verwendet, umError
zu beschreiben, wie es fehlgeschlagen ist, würde zurückkehrenstd::variant<Error, Result>
. - Verwenden Sie Assert und Crash. Wird in p4est und einigen Teilen von igraph verwendet.
Probleme bei jedem Ansatz:
- Das Überprüfen auf jeden Fehler führt zu viel zusätzlichem Code. Die Werte, in denen ein Ergebnis gespeichert wird, müssen immer zuerst deklariert werden, wodurch viele temporäre Variablen eingeführt werden, die möglicherweise nur einmal verwendet werden. Dieser Ansatz erklärt, welcher Fehler aufgetreten ist, es kann jedoch schwierig sein, festzustellen, warum oder bei einem Deep-Call-Stack wo.
- Der Fehlerfall ist leicht zu ignorieren. Darüber hinaus können viele Funktionen nicht einmal einen aussagekräftigen Sentinel-Wert haben, wenn der gesamte Bereich der Ausgabetypen ein plausibles Ergebnis ist. Viele der gleichen Probleme wie # 1.
- Nur in C ++, Python usw. möglich, nicht in C oder Fortran. Kann in C mit setjmp / longjmp Hexerei oder libunwind nachgeahmt werden .
- Nur in C ++, Rust, OCaml usw. möglich, nicht in C oder Fortran. Kann mit Makro-Zauberei in C nachgeahmt werden.
- Wohl das informativste. Wenn Sie diesen Ansatz beispielsweise für eine C-Bibliothek anwenden, für die Sie dann einen Python-Wrapper schreiben, führt ein dummer Fehler wie das Übergeben eines Index außerhalb der Grenzen an ein Array zum Absturz des Python-Interpreters.
Ein Großteil der Ratschläge im Internet zur Fehlerbehandlung wird aus Sicht von Betriebssystemen, eingebetteter Entwicklung oder Webanwendungen verfasst. Abstürze sind inakzeptabel und Sie müssen sich um die Sicherheit sorgen. Wissenschaftliche Anwendungen haben diese Probleme nicht oder nur annähernd in gleichem Maße.
Eine weitere Überlegung ist, welche Arten von Fehlern behoben werden können oder nicht. Ein Malloc-Fehler kann nicht wiederhergestellt werden, und in jedem Fall wird der Killer mit zu wenig Speicher im Betriebssystem darauf zugreifen, bevor Sie dies tun. Ein Index außerhalb der Grenzen für eine Arraygröße kann ebenfalls nicht wiederhergestellt werden. Für mich als Benutzer ist das Schönste, was eine Bibliothek tun kann, mit einer informativen Fehlermeldung abzustürzen. Andererseits könnte das Versagen beispielsweise der Konvergenz eines iterativen linearen Lösers durch Verwendung eines direkten Faktorisierungslösers behoben werden.
Wie sollten wissenschaftliche Bibliotheken Fehler melden und erwarten, dass sie behandelt werden? Mir ist natürlich klar, dass es davon abhängt, in welcher Sprache die Bibliothek implementiert ist. Aber soweit ich das beurteilen kann, möchten die Leute sie für eine ausreichend nützliche Bibliothek aus einer anderen Sprache als der, in der sie implementiert ist, aufrufen.
Abgesehen davon denke ich, dass Ansatz Nr. 5 für eine C-Bibliothek erheblich verbessert werden kann, wenn er einen globalen Assertion-Handler-Funktionszeiger als Teil der öffentlichen API definiert. Der Assertion-Handler meldet standardmäßig die Datei- / Zeilennummer und stürzt ab. Die C ++ - Bindungen für diese Bibliothek würden einen neuen Assertion-Handler definieren, der stattdessen eine C ++ - Ausnahme auslöst. Ebenso würden die Python-Bindungen einen Assertion-Handler definieren, der die CPython-API verwendet, um eine Python-Ausnahme auszulösen. Aber ich kenne keine Beispiele, die diesen Ansatz verfolgen.
Antworten:
Ich gebe Ihnen meine Perspektive, die in dem Deal.II-Projekt, auf das Sie verweisen, verschlüsselt ist.
Erstens gibt es zwei Arten von Fehlerbedingungen: Fehler, die behoben werden können, und Fehler, die nicht behoben werden können.
Ersteres gilt beispielsweise, wenn eine Eingabedatei nicht gelesen werden kann - beispielsweise, wenn Sie Informationen aus einer solchen Datei lesen
$HOME/.dealii
, die möglicherweise vorhanden ist oder nicht. Die Lesefunktion sollte nur zur aufrufenden Funktion zurückkehren, damit diese herausfindet, was zu tun ist. Es kann auch sein, dass eine Ressource derzeit nicht verfügbar ist, aber in einer Minute erneut verfügbar ist (ein remote bereitgestelltes Dateisystem).Letzteres ist beispielsweise der Fall, wenn Sie versuchen, einem Vektor der Größe 20 einen Vektor der Größe 10 hinzuzufügen: Versuchen Sie, wie Sie möchten, es kann nichts dagegen unternommen werden - es gibt einen Fehler im Code, der dazu geführt hat der Punkt, an dem wir versucht haben, die Addition durchzuführen.
Diese beiden Bedingungen sollten unabhängig von der verwendeten Programmiersprache unterschiedlich behandelt werden:
Beenden Sie im zweiten Fall das Programm, da kein Rückgriff erfolgt. Sie können dies tun, indem Sie eine Ausnahme auslösen oder einen Fehlercode zurückgeben, der dem Aufrufer anzeigt, dass nichts getan werden kann. Sie können das Programm jedoch auch sofort abbrechen, da dies dem Programmierer das Debuggen des Problems erheblich erleichtert.
Im ersteren Fall ist eine Ausnahmesituation aufgetreten, die behandelt werden könnte. Obwohl C und Fortran keine Möglichkeit hatten, dies auszudrücken, haben alle vernünftigen Sprachen, die später kamen, Wege in den Sprachstandard aufgenommen, um mit solchen "außergewöhnlichen" Renditen umzugehen, indem sie "Ausnahmen" vorsahen. Verwenden Sie diese - dafür sind sie da; Sie sind auch so konzipiert, dass Sie nicht vergessen können, sie zu ignorieren (wenn Sie dies tun, verbreitet sich die Ausnahme nur eine Ebene höher).
Mit anderen Worten, was ich hier befürworte (und was Deal.II tut), ist eine Mischung Ihrer Strategien 3 und 5, je nach Kontext. Es ist wahr, dass 3 in Sprachen wie C oder Fortran nicht funktioniert. In diesem Fall kann man argumentieren, dass dies ein guter Grund ist, keine Sprachen zu verwenden, die es schwierig machen, auszudrücken, was Sie tun möchten.
Assert
, eine Ausnahme zu machen , anstatt aufzurufenabort()
.)quelle
std::exception
, und diese können durch Bezugnahme abgefangen werden, ohne den abgeleiteten Typ zu kennen.