Wie sollen Fehler in wissenschaftlichen Bibliotheken gemeldet werden?

11

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:

  1. Gibt einen Fehlercode mit dem Ergebnis zurück, das von einem Zeigerargument zurückgegeben wird. Dies ist, was PETSc tut.
  2. Rückgabe von Fehlern durch einen Sentinel-Wert. Zum Beispiel gibt malloc NULL zurück, wenn es keinen Speicher zuordnen konnte, sqrtgibt NaN zurück, wenn Sie eine negative Zahl übergeben usw. Dieser Ansatz wird in vielen libc-Funktionen verwendet.
  3. Ausnahmen auslösen. Wird in Deal.II, Trilinos usw. verwendet.
  4. Geben Sie einen Variantentyp zurück. Beispiel: Eine C ++ - Funktion, die ein Objekt vom Typ zurückgibt, Resultwenn es ordnungsgemäß ausgeführt wird, und einen Typ verwendet, um Errorzu beschreiben, wie es fehlgeschlagen ist, würde zurückkehren std::variant<Error, Result>.
  5. Verwenden Sie Assert und Crash. Wird in p4est und einigen Teilen von igraph verwendet.

Probleme bei jedem Ansatz:

  1. 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.
  2. 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.
  3. Nur in C ++, Python usw. möglich, nicht in C oder Fortran. Kann in C mit setjmp / longjmp Hexerei oder libunwind nachgeahmt werden .
  4. Nur in C ++, Rust, OCaml usw. möglich, nicht in C oder Fortran. Kann mit Makro-Zauberei in C nachgeahmt werden.
  5. 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.

Daniel Shapero
quelle
Eine weitere Überlegung sind Leistungsauswirkungen. Wie wirken sich diese verschiedenen Methoden auf die Geschwindigkeit der Software aus? Sollten wir bei der "Steuerung" von Teilen des Codes (z. B. beim Verarbeiten von Eingabedateien) eine andere Fehlerbehandlung verwenden als bei den rechenintensiven "Engines"?
LedHead
Beachten Sie, dass sich die beste Antwort je nach Sprache unterscheidet.
Chrylis -on Streik-

Antworten:

10

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.

x), aber da der Evaluator wiederholt aufgerufen werden muss, sollte er nicht nur abstürzen, sondern nur eine Ausnahme auslösen. In solchen Fällen sollte eine Ausnahme ausgelöst werden, anstatt das Programm abzubrechen, obwohl die Übergabe eines negativen Werts nicht wiederherstellbar ist. Ich war vor ein paar Jahren mit dieser Haltung nicht einverstanden, habe aber meine Meinung geändert, nachdem die Richtlinien der xSDK-Community-Software die Anforderung kodiert hatten, dass Programme niemals abstürzen sollten (oder zumindest eine Möglichkeit haben sollten, von Absturz zu Ausnahme zu wechseln - also handeln Sie. II hat jetzt die Möglichkeit Assert, eine Ausnahme zu machen , anstatt aufzurufen abort().)

Wolfgang Bangerth
quelle
Ich würde nur das Gegenteil empfehlen: eine Ausnahme auslösen, wenn die Situation nicht behandelt werden kann, und einen Fehlercode zurückgeben, wenn sie behandelt werden kann. Das Problem ist, dass der Umgang mit ausgelösten Ausnahmen schwierig ist: Der Anwendungsprogrammierer muss den Typ aller möglichen Ausnahmen kennen, um sie abzufangen und zu behandeln, sonst stürzt das Programm einfach ab. Crashing ok ist und begrüßt auch für Situationen , die behandelt werden können, weil der Absturz Punkt out-of-the-box mit Python berichtet wird, zum Beispiel, aber für Situationen , die können behandelt werden, ist es (meistens) nicht erlaubt.
Cdalitz
@cdalitz: Es ist ein Konstruktionsfehler von C ++, dass Sie Objekte jeden Typs werfen können. Jede vernünftige Software (Trilinos ausgeschlossen) löst jedoch nur Ausnahmen aus, von denen abgeleitet wird std::exception, und diese können durch Bezugnahme abgefangen werden, ohne den abgeleiteten Typ zu kennen.
Wolfgang Bangerth
1
Ich bin jedoch aus den in der ursprünglichen Frage genannten Gründen nicht damit einverstanden, einen Fehlercode zurückzugeben: (i) Fehlercodes werden viel zu oft ignoriert und infolgedessen werden Fehler überhaupt nicht behandelt. (ii) In vielen Fällen gibt es einfach keinen außergewöhnlichen Wert, der vernünftigerweise zurückgegeben werden kann, da der Rückgabetyp der Funktion festgelegt ist. (iii) Funktionen haben unterschiedliche Rückgabetypen, und Sie müssten jeweils separat definieren, welcher "außergewöhnliche" Wert einen Fehler darstellt.
Wolfgang Bangerth
WB schrieb (sorry, der '@' Trick funktioniert aus irgendeinem Grund nicht und der Benutzername wird aus irgendeinem Grund von StackExchage entfernt): "Fehlercodes werden viel zu oft ignoriert". Dies gilt umso mehr für das Abfangen von Ausnahmen: Nicht viele Softwareentwickler haben die Mühe, jeden Funktionsaufruf in einem Try / Catch-Block in Klammern zu setzen. Aber es ist meistens Geschmackssache: Solange in der Dokumentation klar angegeben ist, ob und welche Ausnahmen eine Funktion auslöst, kann ich damit umgehen. Aber auch hier könnte man sagen: Die Pflicht, Unterlagen zu schreiben, wird viel zu oft ignoriert
;-)
Der Punkt ist jedoch, dass es keine nachgelagerten Probleme gibt, wenn Sie vergessen, eine Ausnahme abzufangen: Das Programm wird einfach abgebrochen. Es wird leicht zu finden sein, wo das Problem aufgetreten ist. Wenn Sie vergessen, den Fehlercode zu überprüfen, kann Ihr Programm zu einem späteren Zeitpunkt aufgrund eines undefinierten internen Status abstürzen - aber wo das ursprüngliche Problem war, bleibt völlig unklar. Es ist außerordentlich schwer, solche Fehler zu finden.
Wolfgang Bangerth