Was halten Sie für "Best Practice", wenn es darum geht, Fehler in einer C-Bibliothek auf konsistente Weise zu behandeln?
Ich habe über zwei Möglichkeiten nachgedacht:
Geben Sie immer den Fehlercode zurück. Eine typische Funktion würde folgendermaßen aussehen:
MYAPI_ERROR getObjectSize(MYAPIHandle h, int* returnedSize);
Die bieten immer einen Fehlerzeiger-Ansatz:
int getObjectSize(MYAPIHandle h, MYAPI_ERROR* returnedError);
Bei Verwendung des ersten Ansatzes ist es möglich, Code wie diesen zu schreiben, bei dem die Fehlerbehandlungsprüfung direkt auf dem Funktionsaufruf platziert wird:
int size;
if(getObjectSize(h, &size) != MYAPI_SUCCESS) {
// Error handling
}
Was hier besser aussieht als der Fehlerbehandlungscode.
MYAPIError error;
int size;
size = getObjectSize(h, &error);
if(error != MYAPI_SUCCESS) {
// Error handling
}
Ich denke jedoch, dass die Verwendung des Rückgabewerts für die Rückgabe von Daten den Code besser lesbar macht. Es ist offensichtlich, dass im zweiten Beispiel etwas in die Größenvariable geschrieben wurde.
Haben Sie Ideen, warum ich einen dieser Ansätze bevorzugen oder sie vielleicht mischen oder etwas anderes verwenden sollte? Ich bin kein Fan von globalen Fehlerzuständen, da dies die Verwendung der Bibliothek mit mehreren Threads schmerzhafter macht.
EDIT: C ++ spezifische Ideen dazu wären auch interessant zu hören, solange sie keine Ausnahmen beinhalten, da es für mich im Moment keine Option ist ...
quelle
Antworten:
Ich mag den Fehler als Rückgabewert. Wenn Sie die API entwerfen und Ihre Bibliothek so einfach wie möglich nutzen möchten, denken Sie an diese Ergänzungen:
Speichern Sie alle möglichen Fehlerzustände in einer typisierten Aufzählung und verwenden Sie sie in Ihrer Bibliothek. Geben Sie nicht nur Ints oder noch schlimmer zurück, sondern mischen Sie Ints oder andere Aufzählungen mit Return-Codes.
Stellen Sie eine Funktion bereit, die Fehler in etwas lesbares umwandelt. Kann einfach sein. Nur Fehler-Aufzählung in, const char * out.
Ich weiß, dass diese Idee die Verwendung von Multithreads etwas erschwert, aber es wäre schön, wenn der Anwendungsprogrammierer einen globalen Fehlerrückruf festlegen könnte. Auf diese Weise können sie während der Fehlersuche einen Haltepunkt in den Rückruf einfügen.
Ich hoffe es hilft.
quelle
Ich habe beide Ansätze verwendet und beide haben für mich gut funktioniert. Unabhängig davon, welches ich verwende, versuche ich immer, dieses Prinzip anzuwenden:
Wenn die einzig möglichen Fehler Programmiererfehler sind, geben Sie keinen Fehlercode zurück, sondern verwenden Sie Asserts innerhalb der Funktion.
Eine Behauptung, die die Eingaben validiert, kommuniziert klar, was die Funktion erwartet, während zu viele Fehlerprüfungen die Programmlogik verdecken können. Die Entscheidung, was für all die verschiedenen Fehlerfälle zu tun ist, kann das Design wirklich komplizieren. Warum sollten Sie herausfinden, wie functionX mit einem Nullzeiger umgehen soll, wenn Sie stattdessen darauf bestehen können, dass der Programmierer niemals einen übergibt?
quelle
assert(X)
wo X eine gültige C-Aussage ist, dass Sie wahr sein wollen. Siehe stackoverflow.com/q/1571340/10396 .assert(X!=NULL);
oderassert(Y<enumtype_MAX);
? Sehen Sie diese Antwort auf Programmierer und die Frage es für weitere Einzelheiten Links zu auf , warum ich denke , das ist der richtige Weg zu gehen.Es gibt eine schöne Reihe von Folien aus dem CERT der CMU mit Empfehlungen für die Verwendung der gängigen C- (und C ++) Fehlerbehandlungstechniken. Eine der besten Folien ist dieser Entscheidungsbaum:
Ich persönlich würde zwei Dinge an diesem Flusswagen ändern.
Zunächst möchte ich klarstellen, dass Objekte manchmal Rückgabewerte verwenden sollten, um Fehler anzuzeigen. Wenn eine Funktion nur Daten aus einem Objekt extrahiert, das Objekt jedoch nicht mutiert, ist die Integrität des Objekts selbst nicht gefährdet, und die Angabe von Fehlern mithilfe eines Rückgabewerts ist angemessener.
Zweitens ist es nicht immer angemessen, Ausnahmen in C ++ zu verwenden. Ausnahmen sind gut, da sie die Menge an Quellcode reduzieren können, die für die Fehlerbehandlung aufgewendet wird, die Funktionssignaturen meist nicht beeinflussen und sehr flexibel sind, welche Daten sie an den Callstack weitergeben können. Auf der anderen Seite sind Ausnahmen aus mehreren Gründen möglicherweise nicht die richtige Wahl:
C ++ - Ausnahmen haben eine ganz bestimmte Semantik. Wenn Sie diese Semantik nicht möchten, sind C ++ - Ausnahmen eine schlechte Wahl. Eine Ausnahme muss sofort nach dem Auslösen behoben werden, und das Design bevorzugt den Fall, dass ein Fehler erforderlich ist, um den Callstack einige Ebenen abzuwickeln.
C ++ - Funktionen, die Ausnahmen auslösen, können später nicht umbrochen werden, um keine Ausnahmen auszulösen, zumindest nicht, ohne die vollen Kosten für Ausnahmen zu bezahlen. Funktionen, die Fehlercodes zurückgeben, können umbrochen werden, um C ++ - Ausnahmen auszulösen, wodurch sie flexibler werden. C ++ 's
new
macht dies richtig, indem es eine nicht werfende Variante bereitstellt.C ++ - Ausnahmen sind relativ teuer, aber dieser Nachteil ist meistens für Programme, die Ausnahmen sinnvoll nutzen, übertrieben. Ein Programm sollte einfach keine Ausnahmen in einen Codepfad werfen, in dem die Leistung ein Problem darstellt. Es spielt keine Rolle, wie schnell Ihr Programm einen Fehler melden und beenden kann.
Manchmal sind keine C ++ - Ausnahmen verfügbar. Entweder sind sie in der C ++ - Implementierung buchstäblich nicht verfügbar, oder die Code-Richtlinien verbieten sie.
Da es sich bei der ursprünglichen Frage um einen Multithread-Kontext handelte, wurde die lokale Fehlerindikatortechnik (wie in der Antwort von SirDarius beschrieben ) in den ursprünglichen Antworten meiner Meinung nach unterschätzt. Es ist threadsicher, erzwingt nicht, dass der Fehler sofort vom Aufrufer behoben wird, und kann beliebige Daten bündeln, die den Fehler beschreiben. Der Nachteil ist, dass es von einem Objekt gehalten werden muss (oder ich nehme an, dass es irgendwie extern zugeordnet ist) und wohl leichter zu ignorieren ist als ein Rückkehrcode.
quelle
Ich verwende den ersten Ansatz, wenn ich eine Bibliothek erstelle. Die Verwendung einer typisierten Aufzählung als Rückkehrcode bietet mehrere Vorteile.
Wenn die Funktion eine kompliziertere Ausgabe wie ein Array und deren Länge zurückgibt, müssen Sie keine beliebigen Strukturen erstellen, um zurückzukehren.
Es ermöglicht eine einfache, standardisierte Fehlerbehandlung.
Es ermöglicht eine einfache Fehlerbehandlung in der Bibliotheksfunktion.
Durch die Verwendung einer typisierten Aufzählung kann der Name der Aufzählung auch im Debugger angezeigt werden. Dies ermöglicht ein einfacheres Debuggen, ohne ständig eine Header-Datei zu konsultieren. Eine Funktion zum Übersetzen dieser Aufzählung in eine Zeichenfolge ist ebenfalls hilfreich.
Das wichtigste Thema unabhängig vom verwendeten Ansatz ist die Konsistenz. Dies gilt für die Benennung von Funktionen und Argumenten, die Reihenfolge der Argumente und die Fehlerbehandlung.
quelle
Verwenden Sie setjmp .
http://en.wikipedia.org/wiki/Setjmp.h
http://aszt.inf.elte.hu/~gsd/halado_cpp/ch02s03.html
http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html
quelle
ETRY
Code wurde überarbeitet, seit diese Antwort geschrieben wurde.setjmp
ist teuer, selbst wenn kein Fehler jemals ausgelöst wird, wird es ziemlich viel CPU-Zeit und Stapelspeicher verbrauchen. Wenn Sie gcc für Windows verwenden, können Sie zwischen verschiedenen Ausnahmehandlungsmethoden für C ++ wählen, von denen eine darauf basiert,setjmp
und Ihr Code wird in der Praxis um bis zu 30% langsamer.Ich persönlich bevorzuge den früheren Ansatz (Rückgabe eines Fehlerindikators).
Falls erforderlich, sollte das Rückgabeergebnis nur darauf hinweisen, dass ein Fehler aufgetreten ist, wobei eine andere Funktion verwendet wird, um den genauen Fehler herauszufinden.
In Ihrem getSize () -Beispiel würde ich berücksichtigen, dass Größen immer Null oder positiv sein müssen, sodass die Rückgabe eines negativen Ergebnisses einen Fehler anzeigen kann, ähnlich wie dies bei UNIX-Systemaufrufen der Fall ist.
Ich kann mir keine Bibliothek vorstellen, die ich für den letzteren Ansatz mit einem als Zeiger übergebenen Fehlerobjekt verwendet habe.
stdio
usw. gehen alle mit einem Rückgabewert.quelle
Wenn ich Programme schreibe, spinne ich während der Initialisierung normalerweise einen Thread zur Fehlerbehandlung ab und initialisiere eine spezielle Struktur für Fehler, einschließlich einer Sperre. Wenn ich dann einen Fehler über Rückgabewerte erkenne, gebe ich die Informationen aus der Ausnahme in die Struktur ein und sende ein SIGIO an den Ausnahmebehandlungsthread. Überprüfen Sie dann, ob ich die Ausführung nicht fortsetzen kann. Wenn ich nicht kann, sende ich ein SIGURG an den Ausnahmethread, wodurch das Programm ordnungsgemäß gestoppt wird.
quelle
Die Rückgabe von Fehlercode ist der übliche Ansatz für die Fehlerbehandlung in C.
Aber kürzlich haben wir auch mit dem Ansatz für ausgehende Fehlerzeiger experimentiert.
Es hat einige Vorteile gegenüber dem Rückgabewertansatz:
Sie können den Rückgabewert für aussagekräftigere Zwecke verwenden.
Wenn Sie diesen Fehlerparameter ausschreiben müssen, werden Sie daran erinnert, den Fehler zu behandeln oder zu verbreiten. (Sie vergessen nie, den Rückgabewert von zu überprüfen
fclose
, nicht wahr?)Wenn Sie einen Fehlerzeiger verwenden, können Sie ihn beim Aufrufen von Funktionen weitergeben. Wenn eine der Funktionen dies festlegt, geht der Wert nicht verloren.
Durch Festlegen eines Datenunterbrechungspunkts für die Fehlervariable können Sie feststellen, wo der Fehler zuerst aufgetreten ist. Durch Festlegen eines bedingten Haltepunkts können Sie auch bestimmte Fehler abfangen.
Dies erleichtert die Automatisierung der Überprüfung, ob Sie alle Fehler behandeln. Die Codekonvention kann Sie zwingen, Ihren Fehlerzeiger als aufzurufen,
err
und es muss das letzte Argument sein. Das Skript kann also mit der Zeichenfolge übereinstimmen underr);
dann prüfen, ob darauf gefolgt wirdif (*err
. In der Praxis haben wir ein Makro namensCER
(check err return) undCEG
(check err goto) erstellt. Sie müssen es also nicht immer eingeben, wenn wir nur auf Fehler zurückkommen möchten, und können die visuelle Unordnung verringern.Nicht alle Funktionen in unserem Code haben diesen ausgehenden Parameter. Diese ausgehenden Parameter werden für Fälle verwendet, in denen Sie normalerweise eine Ausnahme auslösen würden.
quelle
Ich habe in der Vergangenheit viel C-Programmierung gemacht. Und ich habe den Rückgabewert des Fehlercodes wirklich geschätzt. Aber es gibt mehrere mögliche Fallstricke:
quelle
Der UNIX-Ansatz ist Ihrem zweiten Vorschlag am ähnlichsten. Geben Sie entweder das Ergebnis oder einen einzelnen Wert für "Es ist schief gelaufen" zurück. Beispielsweise gibt open den Dateideskriptor bei Erfolg oder -1 bei Fehler zurück. Bei einem Fehler wird auch
errno
eine externe globale Ganzzahl festgelegt, die angibt, welcher Fehler aufgetreten ist.Für das, was es wert ist, hat Cocoa auch einen ähnlichen Ansatz gewählt. Eine Reihe von Methoden geben BOOL zurück und nehmen einen
NSError **
Parameter, so dass sie bei einem Fehler den Fehler setzen und NO zurückgeben. Dann sieht die Fehlerbehandlung so aus:Das liegt irgendwo zwischen deinen beiden Optionen :-).
quelle
Ich habe kürzlich auch über dieses Problem nachgedacht und einige Makros für C geschrieben, die die Semantik von Try-Catch-finally mit rein lokalen Rückgabewerten simulieren . Ich hoffe, Sie finden es nützlich.
quelle
Hier ist ein Ansatz, den ich interessant finde und der etwas Disziplin erfordert.
Dies setzt voraus, dass eine Variable vom Typ Handle die Instanz ist, auf der alle API-Funktionen ausgeführt werden.
Die Idee ist, dass die Struktur hinter dem Handle den vorherigen Fehler als Struktur mit den erforderlichen Daten (Code, Nachricht ...) speichert und dem Benutzer eine Funktion bereitgestellt wird, die einen Zeiger auf dieses Fehlerobjekt zurückgibt. Bei jeder Operation wird das spitze Objekt aktualisiert, sodass der Benutzer seinen Status überprüfen kann, ohne Funktionen aufzurufen. Im Gegensatz zum Fehlermuster ist der Fehlercode nicht global, wodurch der Ansatz threadsicher ist, solange jedes Handle ordnungsgemäß verwendet wird.
Beispiel:
quelle
Erster Ansatz ist meiner Meinung nach besser:
quelle
Ich bevorzuge definitiv die erste Lösung:
Ich würde es leicht modifizieren, um:
Außerdem werde ich niemals einen legitimen Rückgabewert mit einem Fehler mischen, selbst wenn der derzeitige Funktionsumfang dies zulässt. Sie wissen nie, in welche Richtung die Funktionsimplementierung in Zukunft gehen wird.
Und wenn wir bereits über Fehlerbehandlung sprechen, würde ich
goto Error;
als Fehlerbehandlungscode vorschlagen , es sei denn, eineundo
Funktion kann aufgerufen werden, um die Fehlerbehandlung korrekt zu behandeln.quelle
Anstatt Ihren Fehler zurückzugeben und Ihnen somit die Rückgabe von Daten mit Ihrer Funktion zu verbieten, können Sie einen Wrapper für Ihren Rückgabetyp verwenden:
Dann in der aufgerufenen Funktion:
Bitte beachten Sie, dass der Wrapper mit der folgenden Methode die Größe von MyType plus einem Byte (bei den meisten Compilern) hat, was sehr rentabel ist. und Sie müssen kein weiteres Argument auf den Stapel setzen, wenn Sie Ihre Funktion aufrufen (
returnedSize
oderreturnedError
in beiden von Ihnen vorgestellten Methoden).quelle
Hier ist ein einfaches Programm, um die ersten beiden Kugeln von Nils Pipenbrincks Antwort hier zu demonstrieren .
Seine ersten 2 Kugeln sind:
Angenommen, Sie haben ein Modul mit dem Namen geschrieben
mymodule
. Zunächst definieren Sie in mymodule.h Ihre aufzählungsbasierten Fehlercodes und schreiben einige Fehlerzeichenfolgen, die diesen Codes entsprechen. Hier verwende ich ein Array von C-Zeichenfolgen (char *
), das nur dann gut funktioniert, wenn Ihr erster auf Enum basierender Fehlercode den Wert 0 hat und Sie die Zahlen danach nicht mehr bearbeiten. Wenn Sie Fehlercode-Nummern mit Lücken oder anderen Startwerten verwenden, müssen Sie einfach von der Verwendung eines zugeordneten C-String-Arrays (wie unten beschrieben) zur Verwendung einer Funktion wechseln, die eine switch-Anweisung oder if / else if-Anweisungen verwendet von Enum-Fehlercodes auf druckbare C-Strings abzubilden (was ich nicht demonstriere). Es ist deine Entscheidung.mymodule.h
mymodule.c enthält meine Zuordnungsfunktion zum Zuordnen von Enum-Fehlercodes zu druckbaren C-Zeichenfolgen:
mymodule.c
main.c enthält ein Testprogramm, um zu demonstrieren, wie einige Funktionen aufgerufen und einige Fehlercodes daraus gedruckt werden:
Haupt c
Ausgabe:
Verweise:
Sie können diesen Code hier selbst ausführen: https://onlinegdb.com/ByEbKLupS .
quelle
Zusätzlich zu dem, was gesagt wurde, lösen Sie vor der Rückgabe Ihres Fehlercodes eine Bestätigung oder eine ähnliche Diagnose aus, wenn ein Fehler zurückgegeben wird, da dies die Ablaufverfolgung erheblich vereinfacht. Die Art und Weise, wie ich dies tue, besteht darin, eine angepasste Zusicherung zu haben, die bei der Veröffentlichung noch kompiliert wird, aber nur ausgelöst wird, wenn sich die Software im Diagnosemodus befindet, mit der Option, stillschweigend in eine Protokolldatei zu berichten oder auf dem Bildschirm anzuhalten.
Ich persönlich gebe Fehlercodes als negative Ganzzahlen mit no_error als Null zurück, aber es bleibt Ihnen der mögliche folgende Fehler
Eine Alternative besteht darin, einen Fehler immer als Null zurückzugeben und eine LastError () - Funktion zu verwenden, um Details zum tatsächlichen Fehler bereitzustellen.
quelle
Ich bin mehrmals auf diese Fragen und Antworten gestoßen und wollte eine umfassendere Antwort liefern. Ich denke, der beste Weg, darüber nachzudenken, ist, wie man Fehler an den Anrufer zurückgibt und was Sie zurückgeben.
Wie
Es gibt drei Möglichkeiten, Informationen von einer Funktion zurückzugeben:
Rückgabewert
Sie können nur den Wert eines einzelnen Objekts zurückgeben, es kann sich jedoch um einen beliebigen Komplex handeln. Hier ist ein Beispiel für eine Fehlerrückgabefunktion:
Ein Vorteil von Rückgabewerten besteht darin, dass Aufrufe für eine weniger aufdringliche Fehlerbehandlung verkettet werden können:
Dies betrifft nicht nur die Lesbarkeit, sondern kann auch die einheitliche Verarbeitung eines Arrays solcher Funktionszeiger ermöglichen.
Out Argument (s)
Sie können mehr über mehr als ein Objekt über Argumente zurückgeben. Es wird jedoch empfohlen, die Gesamtzahl der Argumente niedrig zu halten (z. B. <= 4):
Außerhalb der Bandbreite
Mit setjmp () definieren Sie einen Ort und wie Sie mit einem int-Wert umgehen möchten, und übertragen die Steuerung über longjmp () an diesen Ort. Siehe Praktische Verwendung von setjmp und longjmp in C. .
Was
Indikator
Eine Fehleranzeige zeigt Ihnen nur an, dass ein Problem vorliegt, aber nichts über die Art des Problems:
Dies ist die am wenigsten leistungsfähige Methode für eine Funktion, um den Fehlerstatus zu kommunizieren. Sie ist jedoch perfekt, wenn der Anrufer ohnehin nicht schrittweise auf den Fehler reagieren kann.
Code
Ein Fehlercode informiert den Anrufer über die Art des Problems und ermöglicht möglicherweise eine geeignete Antwort (von oben). Dies kann ein Rückgabewert sein oder wie das Beispiel look_ma () über einem Fehlerargument.
Objekt
Mit einem Fehlerobjekt kann der Anrufer über beliebig komplizierte Probleme informiert werden. Zum Beispiel ein Fehlercode und eine geeignete lesbare Nachricht. Es kann den Anrufer auch darüber informieren, dass mehrere Fehler aufgetreten sind oder ein Fehler pro Element bei der Verarbeitung einer Sammlung aufgetreten ist:
Anstatt das Fehlerarray vorab zuzuweisen, können Sie es natürlich auch nach Bedarf dynamisch (neu) zuweisen.
Zurückrufen
Rückruf ist die leistungsstärkste Methode, um Fehler zu behandeln, da Sie der Funktion mitteilen können, welches Verhalten bei einem Fehler auftreten soll. Jeder Funktion kann ein Rückrufargument hinzugefügt werden, oder wenn eine Anpassung nur pro Instanz einer Struktur wie dieser erforderlich ist:
Ein interessanter Vorteil eines Rückrufs besteht darin, dass er mehrmals oder gar nicht aufgerufen werden kann, wenn keine Fehler vorliegen, bei denen auf dem glücklichen Pfad kein Overhead entsteht.
Es gibt jedoch eine Umkehrung der Kontrolle. Der aufrufende Code weiß nicht, ob der Rückruf aufgerufen wurde. Daher kann es sinnvoll sein, auch einen Indikator zu verwenden.
quelle
BEARBEITEN: Wenn Sie nur auf den letzten Fehler zugreifen müssen und nicht in einer Multithread-Umgebung arbeiten.
Sie können nur true / false zurückgeben (oder eine Art #define, wenn Sie in C arbeiten und keine Bool-Variablen unterstützen) und einen globalen Fehlerpuffer haben, der den letzten Fehler enthält:
quelle
Mit dem zweiten Ansatz kann der Compiler optimierten Code erzeugen, da der Compiler bei der Übergabe der Adresse einer Variablen an eine Funktion seinen Wert bei nachfolgenden Aufrufen anderer Funktionen nicht in Registern behalten kann. Der Abschlusscode wird normalerweise nur einmal unmittelbar nach dem Anruf verwendet, während "echte" Daten, die vom Anruf zurückgegeben werden, möglicherweise häufiger verwendet werden
quelle
Ich bevorzuge die Fehlerbehandlung in C mit der folgenden Technik:
Quelle: http://blog.staila.com/?p=114
quelle
goto
's anstatt mit ' sif
'. Referenzen: eins , zwei .Zusätzlich zu den anderen guten Antworten schlage ich vor, dass Sie versuchen, das Fehlerflag und den Fehlercode zu trennen, um bei jedem Anruf eine Zeile zu speichern, dh:
Wenn Sie viele Fehler überprüfen, hilft diese kleine Vereinfachung wirklich.
quelle