Fehlerbehandlung im C-Code

152

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 ...

Laserallan
quelle
Ich habe C erst seit ungefähr zwei Wochen gelernt, aber ich habe das Gefühl, dass OUT-Parameter der defacto-Rückgabewert für die meisten Funktionen sind, da dies den Aufwand für die Rückgabe von Strukturen nach Wert vermeidet und verringert die Notwendigkeit, Speicher freizugeben, da sich die meisten Variablen auf dem Stapel befinden. Da ich "return" nicht für den tatsächlichen Wert der Funktion verwende, kann ich es die meiste Zeit zur Fehlerbehandlung verwenden.
Joel Roberts

Antworten:

74

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.

Nils Pipenbrinck
quelle
5
Warum sagen Sie: "Diese Idee macht die Verwendung von Multithreads etwas schwierig." Welches Teil wird durch Multithreading erschwert? Können Sie ein kurzes Beispiel geben?
SayeedHussain
1
@crypticcoder Einfach gesagt: Ein globaler Fehlerrückruf kann in jedem Thread-Kontext aufgerufen werden. Wenn Sie den Fehler nur ausdrucken, treten keine Probleme auf. Wenn Sie versuchen, Probleme zu beheben, müssen Sie herausfinden, welcher aufrufende Thread den Fehler verursacht hat, und das macht die Sache schwierig.
Nils Pipenbrinck
9
Was ist, wenn Sie weitere Details des Fehlers mitteilen möchten? Sie haben beispielsweise einen Parserfehler und möchten die Zeilennummer und die Spalte des Syntaxfehlers sowie eine Möglichkeit angeben, alles gut zu drucken.
Panzi
1
@panzi Dann müssen Sie natürlich eine Struktur zurückgeben (oder einen Out-Zeiger verwenden, wenn die Struktur wirklich groß ist) und eine Funktion haben, um die Struktur als Zeichenfolge zu formatieren.
Flügelspieler Sendon
Ich demonstriere Ihre ersten 2 Aufzählungszeichen im Code hier: stackoverflow.com/questions/385975/error-handling-in-c-code/…
Gabriel Staples
92

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?

AShelly
quelle
1
Haben Sie ein Beispiel für Behauptungen in C? (Ich bin sehr grün zu C)
thomthom
Es ist normalerweise so einfach wie assert(X)wo X eine gültige C-Aussage ist, dass Sie wahr sein wollen. Siehe stackoverflow.com/q/1571340/10396 .
AShelly
14
Ugh, benutze niemals Asserts im Bibliothekscode ! Auch nicht verschiedene Arten der Fehlerbehandlung in einem Stück Code mischte wie andere auch ...
mirabilos
10
Ich bin mir sicher einig, dass ich keine Stile mische. Ich bin neugierig auf Ihre Argumentation zu Behauptungen. Wenn in meiner Funktionsdokumentation "Argument X darf nicht NULL sein" oder "Y muss Mitglied dieser Aufzählung sein" steht, was ist dann falsch an assert(X!=NULL);oder assert(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.
AShelly
8
@AShelly Das Problem mit behauptet, dass sie normalerweise nicht in Release-Builds vorhanden sind.
Calmarius
29

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:

Entscheidungsbaum zur Fehlerbehandlung

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:

  1. 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.

  2. 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 newmacht dies richtig, indem es eine nicht werfende Variante bereitstellt.

  3. 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.

  4. 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.

Praxeolitisch
quelle
5
Möglicherweise stellen Sie fest, dass die C ++ - Codierungsstandards von Google weiterhin besagen, dass keine C ++ - Ausnahmen verwendet werden.
Jonathan Leffler
19

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.

    rc = func(..., int **return_array, size_t *array_length);
  • Es ermöglicht eine einfache, standardisierte Fehlerbehandlung.

    if ((rc = func(...)) != API_SUCCESS) {
       /* Error Handling */
    }
  • Es ermöglicht eine einfache Fehlerbehandlung in der Bibliotheksfunktion.

    /* Check for valid arguments */
    if (NULL == return_array || NULL == array_length)
        return API_INVALID_ARGS;
  • 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.

Jeffrey Cohen
quelle
9

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

#include <setjmp.h>
#include <stdio.h>

jmp_buf x;

void f()
{
    longjmp(x,5); // throw 5;
}

int main()
{
    // output of this program is 5.

    int i = 0;

    if ( (i = setjmp(x)) == 0 )// try{
    {
        f();
    } // } --> end of try{
    else // catch(i){
    {
        switch( i )
        {
        case  1:
        case  2:
        default: fprintf( stdout, "error code = %d\n", i); break;
        }
    } // } --> end of catch(i){
    return 0;
}

#include <stdio.h>
#include <setjmp.h>

#define TRY do{ jmp_buf ex_buf__; if( !setjmp(ex_buf__) ){
#define CATCH } else {
#define ETRY } }while(0)
#define THROW longjmp(ex_buf__, 1)

int
main(int argc, char** argv)
{
   TRY
   {
      printf("In Try Statement\n");
      THROW;
      printf("I do not appear\n");
   }
   CATCH
   {
      printf("Got Exception!\n");
   }
   ETRY;

   return 0;
}
Amir Saniyan
quelle
2
Der zweite Codeblock basiert auf einer früheren Version des Codes auf Francesco Niditos Seite, auf die oben in der Antwort verwiesen wird. Der ETRYCode wurde überarbeitet, seit diese Antwort geschrieben wurde.
Jonathan Leffler
2
Setjmp ist eine schreckliche Fehlerbehandlungsstrategie. Es ist teuer, fehleranfällig (mit nichtflüchtigen geänderten Einheimischen, die ihre geänderten Werte nicht beibehalten) und verliert Ressourcen, wenn Sie zwischen den Aufrufen setjmp und longjmp irgendwelche zuweisen. Sie sollten in der Lage sein, 30 Retouren und Int-Val-Prüfungen durchzuführen, bevor Sie die Kosten für sigjmp / longjmp amortisieren. Die meisten Callstacks gehen nicht so tief, besonders wenn Sie nicht viel Rekursion betreiben (und wenn Sie dies tun, haben Sie andere Perf-Probleme als die Kosten für Retouren + Schecks).
PSkocik
1
Wenn Sie Speicher malloc'ed und dann werfen, wird der Speicher nur für immer lecken. Auch setjmpist 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, setjmpund Ihr Code wird in der Praxis um bis zu 30% langsamer.
Mecki
7

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. stdiousw. gehen alle mit einem Rückgabewert.

Alnitak
quelle
1
Eine Bibliothek, bei der ich den letzteren Ansatz verwendet habe, ist die Maya-Programmier-API. Es ist jedoch eher eine C ++ - Bibliothek als C. Es ist ziemlich inkonsistent, wie es mit seinen Fehlern umgeht, und manchmal wird der Fehler als Rückgabewert übergeben, und manchmal wird das Ergebnis als Referenz übergeben.
Laserallan
1
vergiss nicht strtod, ok, das letzte Argument ist nicht nur für die Anzeige von Fehlern, sondern es tut es auch.
Quinmars
7

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.

Henry
quelle
7

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, errund es muss das letzte Argument sein. Das Skript kann also mit der Zeichenfolge übereinstimmen und err);dann prüfen, ob darauf gefolgt wird if (*err. In der Praxis haben wir ein Makro namens CER(check err return) und CEG(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.

Calmarius
quelle
6

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:

  • Doppelte Fehlernummern, dies kann mit einer globalen Datei error.h behoben werden.
  • Wenn Sie vergessen, den Fehlercode zu überprüfen, sollte dies mit einem Hinweis und langen Debugging-Stunden behoben werden. Aber am Ende werden Sie lernen (oder Sie werden wissen, dass jemand anderes das Debuggen übernimmt).
Toon Krijthe
quelle
2
Das zweite Problem kann durch die richtige Compiler-Warnstufe, den richtigen Codeüberprüfungsmechanismus und die Tools zur statischen Code-Analyse gelöst werden.
Ilya
1
Sie können auch nach dem Prinzip arbeiten: Wenn die API-Funktion aufgerufen wird und der Rückgabewert nicht überprüft wird, liegt ein Fehler vor.
Jonathan Leffler
6

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 errnoeine 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:

NSError *error = nil;
if ([myThing doThingError: &error] == NO)
{
  // error handling
}

Das liegt irgendwo zwischen deinen beiden Optionen :-).


quelle
5

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:

MyHandle * h = MyApiCreateHandle();

/* first call checks for pointer nullity, since we cannot retrieve error code
   on a NULL pointer */
if (h == NULL)
     return 0; 

/* from here h is a valid handle */

/* get a pointer to the error struct that will be updated with each call */
MyApiError * err = MyApiGetError(h);


MyApiFileDescriptor * fd = MyApiOpenFile("/path/to/file.ext");

/* we want to know what can go wrong */
if (err->code != MyApi_ERROR_OK) {
    fprintf(stderr, "(%d) %s\n", err->code, err->message);
    MyApiDestroy(h);
    return 0;
}

MyApiRecord record;

/* here the API could refuse to execute the operation if the previous one
   yielded an error, and eventually close the file descriptor itself if
   the error is not recoverable */
MyApiReadFileRecord(h, &record, sizeof(record));

/* we want to know what can go wrong, here using a macro checking for failure */
if (MyApi_FAILED(err)) {
    fprintf(stderr, "(%d) %s\n", err->code, err->message);
    MyApiDestroy(h);
    return 0;
}
SirDarius
quelle
4

Erster Ansatz ist meiner Meinung nach besser:

  • Auf diese Weise ist es einfacher, Funktionen zu schreiben. Wenn Sie in der Mitte der Funktion einen Fehler bemerken, geben Sie einfach einen Fehlerwert zurück. Beim zweiten Ansatz müssen Sie einem der Parameter einen Fehlerwert zuweisen und dann etwas zurückgeben ... aber was würden Sie zurückgeben - Sie haben keinen korrekten Wert und Sie geben keinen Fehlerwert zurück.
  • Es ist beliebter, damit es leichter zu verstehen und zu warten ist
Klesk
quelle
4

Ich bevorzuge definitiv die erste Lösung:

int size;
if(getObjectSize(h, &size) != MYAPI_SUCCESS) {
  // Error handling
}

Ich würde es leicht modifizieren, um:

int size;
MYAPIError rc;

rc = getObjectSize(h, &size)
if ( rc != MYAPI_SUCCESS) {
  // Error handling
}

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, eine undoFunktion kann aufgerufen werden, um die Fehlerbehandlung korrekt zu behandeln.

Ilya
quelle
3

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:

typedef struct {
    enum {SUCCESS, ERROR} status;
    union {
        int errCode;
        MyType value;
    } ret;
} MyTypeWrapper;

Dann in der aufgerufenen Funktion:

MyTypeWrapper MYAPIFunction(MYAPIHandle h) {
    MyTypeWrapper wrapper;
    // [...]
    // If there is an error somewhere:
    wrapper.status = ERROR;
    wrapper.ret.errCode = MY_ERROR_CODE;

    // Everything went well:
    wrapper.status = SUCCESS;
    wrapper.ret.value = myProcessedData;
    return wrapper;
} 

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 ( returnedSizeoder returnedErrorin beiden von Ihnen vorgestellten Methoden).

Antoine C.
quelle
3

Hier ist ein einfaches Programm, um die ersten beiden Kugeln von Nils Pipenbrincks Antwort hier zu demonstrieren .

Seine ersten 2 Kugeln sind:

  • 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.

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

/// @brief Error codes for library "mymodule"
typedef enum mymodule_error_e
{
    /// No error
    MYMODULE_ERROR_OK = 0,
    
    /// Invalid arguments (ex: NULL pointer where a valid pointer is required)
    MYMODULE_ERROR_INVARG,

    /// Out of memory (RAM)
    MYMODULE_ERROR_NOMEM,

    /// Make up your error codes as you see fit
    MYMODULE_ERROR_MYERROR, 

    // etc etc
    
    /// Total # of errors in this list (NOT AN ACTUAL ERROR CODE);
    /// NOTE: that for this to work, it assumes your first error code is value 0 and you let it naturally 
    /// increment from there, as is done above, without explicitly altering any error values above
    MYMODULE_ERROR_COUNT,
} mymodule_error_t;

// Array of strings to map enum error types to printable strings
// - see important NOTE above!
const char* const MYMODULE_ERROR_STRS[] = 
{
    "MYMODULE_ERROR_OK",
    "MYMODULE_ERROR_INVARG",
    "MYMODULE_ERROR_NOMEM",
    "MYMODULE_ERROR_MYERROR",
};

// To get a printable error string
const char* mymodule_error_str(mymodule_error_t err);

// Other functions in mymodule
mymodule_error_t mymodule_func1(void);
mymodule_error_t mymodule_func2(void);
mymodule_error_t mymodule_func3(void);

mymodule.c enthält meine Zuordnungsfunktion zum Zuordnen von Enum-Fehlercodes zu druckbaren C-Zeichenfolgen:

mymodule.c

#include <stdio.h>

/// @brief      Function to get a printable string from an enum error type
/// @param[in]  err     a valid error code for this module
/// @return     A printable C string corresponding to the error code input above, or NULL if an invalid error code
///             was passed in
const char* mymodule_error_str(mymodule_error_t err)
{
    const char* err_str = NULL;

    // Ensure error codes are within the valid array index range
    if (err >= MYMODULE_ERROR_COUNT)
    {
        goto done;
    }

    err_str = MYMODULE_ERROR_STRS[err];

done:
    return err_str;
}

// Let's just make some empty dummy functions to return some errors; fill these in as appropriate for your 
// library module

mymodule_error_t mymodule_func1(void)
{
    return MYMODULE_ERROR_OK;
}

mymodule_error_t mymodule_func2(void)
{
    return MYMODULE_ERROR_INVARG;
}

mymodule_error_t mymodule_func3(void)
{
    return MYMODULE_ERROR_MYERROR;
}

main.c enthält ein Testprogramm, um zu demonstrieren, wie einige Funktionen aufgerufen und einige Fehlercodes daraus gedruckt werden:

Haupt c

#include <stdio.h>

int main()
{
    printf("Demonstration of enum-based error codes in C (or C++)\n");

    printf("err code from mymodule_func1() = %s\n", mymodule_error_str(mymodule_func1()));
    printf("err code from mymodule_func2() = %s\n", mymodule_error_str(mymodule_func2()));
    printf("err code from mymodule_func3() = %s\n", mymodule_error_str(mymodule_func3()));

    return 0;
}

Ausgabe:

Demonstration von enumbasierten Fehlercodes in C (oder C ++)
Fehlercode von mymodule_func1 () = MYMODULE_ERROR_OK Fehlercode
von mymodule_func2 () = MYMODULE_ERROR_INVARG Fehlercode
von mymodule_func3 () = MYMODULE_ERROR_MYERROR

Verweise:

Sie können diesen Code hier selbst ausführen: https://onlinegdb.com/ByEbKLupS .

Gabriel Staples
quelle
2

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

if (MyFunc())
 DoSomething();

Eine Alternative besteht darin, einen Fehler immer als Null zurückzugeben und eine LastError () - Funktion zu verwenden, um Details zum tatsächlichen Fehler bereitzustellen.

SmacL
quelle
2

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:

  1. Rückgabewert
  2. Out Argument (s)
  3. Out of Band, einschließlich nicht lokaler goto (setjmp / longjmp), Datei- oder globaler Gültigkeitsbereichsvariablen, Dateisystem usw.

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:

  enum error hold_my_beer();

Ein Vorteil von Rückgabewerten besteht darin, dass Aufrufe für eine weniger aufdringliche Fehlerbehandlung verkettet werden können:

  !hold_my_beer() &&
  !hold_my_cigarette() &&
  !hold_my_pants() ||
  abort();

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):

void look_ma(enum error *e, char *what_broke);

enum error e;
look_ma(e);
if(e == FURNITURE) {
  reorder(what_broke);
} else if(e == SELF) {
  tell_doctor(what_broke);
}

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

  1. Indikator
  2. Code
  3. Objekt
  4. Zurückrufen

Indikator

Eine Fehleranzeige zeigt Ihnen nur an, dass ein Problem vorliegt, aber nichts über die Art des Problems:

struct foo *f = foo_init();
if(!f) {
  /// handle the absence of foo
}

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:

struct collection friends;
enum error *e = malloc(c.size * sizeof(enum error));
...
ask_for_favor(friends, reason);
for(int i = 0; i < c.size; i++) {
   if(reason[i] == NOT_FOUND) find(friends[i]);
}

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:

 struct foo {
    ...
    void (error_handler)(char *);
 };

 void default_error_handler(char *message) { 
    assert(f);
    printf("%s", message);
 }

 void foo_set_error_handler(struct foo *f, void (*eh)(char *)) {
    assert(f);
    f->error_handler = eh;
 }

 struct foo *foo_init() {
    struct foo *f = malloc(sizeof(struct foo));
    foo_set_error_handler(f, default_error_handler);
    return f;
 }


 struct foo *f = foo_init();
 foo_something();

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.

Allan Wind
quelle
1

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:

int getObjectSize(MYAPIHandle h, int* returnedSize);
MYAPI_ERROR LastError;
MYAPI_ERROR* getLastError() {return LastError;};
#define FUNC_SUCCESS 1
#define FUNC_FAIL 0

if(getObjectSize(h, &size) != FUNC_SUCCESS ) {
    MYAPI_ERROR* error = getLastError();
    // error handling
}
Igor Oks
quelle
In der Tat, aber es ist nicht C, es kann vom Betriebssystem bereitgestellt werden oder nicht. Wenn Sie beispielsweise an Echtzeitbetriebssystemen arbeiten, haben Sie es möglicherweise nicht.
Ilya
1

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

dmityugov
quelle
1

Ich bevorzuge die Fehlerbehandlung in C mit der folgenden Technik:

struct lnode *insert(char *data, int len, struct lnode *list) {
    struct lnode *p, *q;
    uint8_t good;
    struct {
            uint8_t alloc_node : 1;
            uint8_t alloc_str : 1;
    } cleanup = { 0, 0 };

   // allocate node.
    p = (struct lnode *)malloc(sizeof(struct lnode));
    good = cleanup.alloc_node = (p != NULL);

   // good? then allocate str
    if (good) {
            p->str = (char *)malloc(sizeof(char)*len);
            good = cleanup.alloc_str = (p->str != NULL);
    }

   // good? copy data
    if(good) {
            memcpy ( p->str, data, len );
    }

   // still good? insert in list
    if(good) {
            if(NULL == list) {
                    p->next = NULL;
                    list = p;
            } else {
                    q = list;
                    while(q->next != NULL && good) {
                            // duplicate found--not good
                            good = (strcmp(q->str,p->str) != 0);
                            q = q->next;
                    }
                    if (good) {
                            p->next = q->next;
                            q->next = p;
                    }
            }
    }

   // not-good? cleanup.
    if(!good) {
            if(cleanup.alloc_str)   free(p->str);
            if(cleanup.alloc_node)  free(p);
    }

   // good? return list or else return NULL
    return (good ? list : NULL);
}

Quelle: http://blog.staila.com/?p=114

Nitin Kunal
quelle
1
Gute Technik. Ich finde es sogar ordentlicher mit goto's anstatt mit ' s if'. Referenzen: eins , zwei .
Ant_222
0

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:

if( !doit(a, b, c, &errcode) )
{   (* handle *)
    (* thine  *)
    (* error  *)
}

Wenn Sie viele Fehler überprüfen, hilft diese kleine Vereinfachung wirklich.

Ant_222
quelle