Gestione degli errori nel codice C.


152

Cosa consideri "best practice" quando si tratta di errori nella gestione degli errori in modo coerente in una libreria C.

Ci sono due modi in cui ho pensato:

Restituisce sempre il codice di errore. Una funzione tipica sarebbe simile a questa:

MYAPI_ERROR getObjectSize(MYAPIHandle h, int* returnedSize);

Fornire sempre un approccio puntatore errore:

int getObjectSize(MYAPIHandle h, MYAPI_ERROR* returnedError);

Quando si utilizza il primo approccio è possibile scrivere codice come questo in cui il controllo di gestione degli errori viene inserito direttamente nella chiamata di funzione:

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

Che sembra migliore del codice di gestione degli errori qui.

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

Tuttavia, penso che l'utilizzo del valore restituito per la restituzione dei dati renda il codice più leggibile. È ovvio che qualcosa è stato scritto nella variabile size nel secondo esempio.

Hai qualche idea sul perché dovrei preferire uno di quegli approcci o forse mescolarli o usare qualcos'altro? Non sono un fan degli stati di errore globali dal momento che tende a rendere l'uso della libreria multi-thread molto più doloroso.

EDIT: idee specifiche su C ++ su questo sarebbero anche interessanti da ascoltare fino a quando non comportano eccezioni poiché al momento non è un'opzione per me ...


Sto imparando C da circa due settimane, ma la sensazione che ho ottenuto è che i parametri OUT sono il valore di ritorno defacto per la maggior parte delle funzioni, in quanto evita il sovraccarico di restituire le strutture in base al valore e mitiga la necessità di deallocare memoria poiché la maggior parte delle variabili si trova nello stack. Quindi poiché non sto usando "return" per il valore effettivo della funzione, sono libero di usarlo per la gestione degli errori per la maggior parte del tempo.
Joel Roberts,

Risposte:


74

Mi piace l'errore come valore di ritorno. Se stai progettando l'API e vuoi utilizzare la tua libreria il più indolore possibile, pensa a queste aggiunte:

  • memorizza tutti i possibili stati di errore in un enum dattiloscritto e usalo nella tua libreria. Non solo restituire ints o peggio ancora, mescolare ints o enumerazioni diverse con codici di ritorno.

  • fornire una funzione che converte gli errori in qualcosa di leggibile dall'uomo. Può essere semplice Solo errore-enum in, const char * out.

  • So che questa idea rende l'uso del multithread un po 'difficile, ma sarebbe bello se il programmatore dell'applicazione potesse impostare un callback globale di errori. In questo modo saranno in grado di inserire un breakpoint nel callback durante le sessioni di caccia ai bug.

Spero che sia d'aiuto.


5
Perché dici "questa idea rende l'uso multi-thread un po 'difficile". Quale parte è resa difficile dal multi-threading? Puoi fare un rapido esempio?
SayeedHussain,

1
@crypticcoder Detto semplicemente: un callback di errore globale può essere invocato in qualunque contesto di thread. Se si stampa l'errore e non si riscontrano problemi. Se provi a correggere i problemi dovrai scoprire quale thread chiamante ha causato l'errore e ciò rende le cose difficili.
Nils Pipenbrinck,

9
Cosa succede se si desidera comunicare ulteriori dettagli sull'errore? Ad esempio, hai un errore del parser e vuoi fornire il numero di riga e la colonna dell'errore di sintassi e un modo per stamparlo tutto bene.
Panzi,

1
@panzi allora ovviamente devi restituire una struct (o usare un puntatore out se struct è veramente grande) e avere una funzione per formattare la struct come una stringa.
Winger Sendon,

Dimostro i tuoi primi 2 proiettili nel codice qui: stackoverflow.com/questions/385975/error-handling-in-c-code/…
Gabriel Staples

92

Ho usato entrambi gli approcci ed entrambi hanno funzionato bene per me. Qualunque sia quello che uso, cerco sempre di applicare questo principio:

Se gli unici errori possibili sono errori del programmatore, non restituire un codice di errore, utilizzare assert all'interno della funzione.

Un'affermazione che convalida gli ingressi comunica chiaramente ciò che la funzione si aspetta, mentre un eccessivo controllo degli errori può oscurare la logica del programma. Decidere cosa fare per tutti i vari casi di errore può davvero complicare la progettazione. Perché capire come functionX dovrebbe gestire un puntatore nullo se si può invece insistere sul fatto che il programmatore non ne passi mai uno?


1
Hai un esempio di affermazioni in C? (Sono molto verde per C)
thomthom

Di solito è semplice come assert(X)dove X è una qualsiasi dichiarazione C valida che vuoi essere vera. vedi stackoverflow.com/q/1571340/10396 .
AShelly,

14
Uff, assolutamente mai usare assert nel codice della libreria ! Inoltre, non mescolare vari stili di gestione degli errori in un pezzo di codice come altri ...
mirabilos

10
Sono certamente d'accordo sul non mescolare gli stili. Sono curioso di sapere il tuo ragionamento sulle affermazioni. Se la documentazione della mia funzione dice "l'argomento X non deve essere NULL" o "Y deve essere un membro di questo enum", allora cosa c'è di sbagliato in assert(X!=NULL);o assert(Y<enumtype_MAX);? Vedi questa risposta sui programmatori e la domanda a cui si collega per maggiori dettagli sul perché penso che sia la strada giusta da percorrere.
AShelly,

8
@AShelly Il problema afferma che di solito non sono presenti nelle build di rilascio.
Calmarius,

29

C'è un bel set di diapositive dal CERT di CMU con consigli su quando usare ciascuna delle comuni tecniche di gestione degli errori C (e C ++). Una delle diapositive migliori è questo albero decisionale:

Errore nella gestione dell'albero decisionale

Personalmente cambierei due cose su questo diagramma di flusso.

Innanzitutto, vorrei chiarire che a volte gli oggetti dovrebbero usare valori di ritorno per indicare errori. Se una funzione estrae solo i dati da un oggetto ma non muta l'oggetto, l'integrità dell'oggetto stesso non è a rischio e indicare errori con un valore di ritorno è più appropriato.

In secondo luogo, non è sempre appropriato utilizzare le eccezioni in C ++. Le eccezioni sono valide perché possono ridurre la quantità di codice sorgente dedicato alla gestione degli errori, per lo più non influiscono sulle firme delle funzioni e sono molto flessibili in quali dati possono trasmettere il callstack. D'altra parte, le eccezioni potrebbero non essere la scelta giusta per alcuni motivi:

  1. Le eccezioni C ++ hanno una semantica molto particolare. Se non si desidera quella semantica, le eccezioni C ++ sono una scelta sbagliata. Un'eccezione deve essere affrontata immediatamente dopo essere stata lanciata e il design favorisce il caso in cui un errore dovrà sciogliere il callstack di alcuni livelli.

  2. Le funzioni C ++ che generano eccezioni non possono essere successivamente raggruppate per non generare eccezioni, almeno non senza comunque pagare l'intero costo delle eccezioni. Le funzioni che restituiscono codici di errore possono essere racchiuse per generare eccezioni C ++, rendendole più flessibili. C ++ newottiene questo diritto fornendo una variante non gettante.

  3. Le eccezioni C ++ sono relativamente costose, ma questo aspetto negativo è per lo più esagerato per i programmi che fanno un uso ragionevole delle eccezioni. Un programma semplicemente non dovrebbe generare eccezioni su un codepath in cui le prestazioni sono un problema. Non importa quanto velocemente il tuo programma possa segnalare un errore ed uscire.

  4. A volte le eccezioni C ++ non sono disponibili. O non sono letteralmente disponibili nell'implementazione del proprio C ++ o le linee guida del proprio codice li vietano.


Dato che la domanda iniziale era di circa un contesto multithread, penso che la tecnica indicatore di errore locale (ciò che è descritto nella SirDarius 's risposta ) è stato sottovalutato nelle risposte originali. È thread-safe, non impone che l'errore venga immediatamente risolto dal chiamante e può raggruppare dati arbitrari che descrivono l'errore. Il rovescio della medaglia è che deve essere trattenuto da un oggetto (o suppongo che sia associato in qualche modo esternamente) ed è probabilmente più facile da ignorare di un codice di ritorno.


5
Potresti notare che gli standard di codifica C ++ di Google dicono ancora che non utilizziamo eccezioni C ++.
Jonathan Leffler,

19

Uso il primo approccio ogni volta che creo una libreria. Ci sono molti vantaggi nell'usare un'enumerazione tipizzata come codice di ritorno.

  • Se la funzione restituisce un output più complicato come un array e la sua lunghezza, non è necessario creare strutture arbitrarie per restituire.

    rc = func(..., int **return_array, size_t *array_length);
  • Consente una gestione degli errori semplice e standardizzata.

    if ((rc = func(...)) != API_SUCCESS) {
       /* Error Handling */
    }
  • Consente una semplice gestione degli errori nella funzione di libreria.

    /* Check for valid arguments */
    if (NULL == return_array || NULL == array_length)
        return API_INVALID_ARGS;
  • L'uso di un enum tipizzato consente inoltre di visualizzare il nome dell'enum nel debugger. Ciò consente un debug più semplice senza la necessità di consultare costantemente un file di intestazione. Anche avere una funzione per tradurre questo enum in una stringa è utile.

La questione più importante, indipendentemente dall'approccio utilizzato, deve essere coerente. Questo vale per la denominazione di funzioni e argomenti, l'ordinamento di argomenti e la gestione degli errori.


9

Usa 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;
}

2
Il secondo blocco di codice si basa su una versione precedente del codice nella pagina di Francesco Nidito a cui si fa riferimento nella parte superiore della risposta. Il ETRYcodice è stato rivisto da quando è stata scritta questa risposta.
Jonathan Leffler,

2
Setjmp è un'orribile strategia di gestione degli errori. È costoso, soggetto a errori (con i locali modificati non volatili che non mantengono i loro valori modificati e tutti) e perde risorse se si allocano tra le chiamate setjmp e longjmp. Dovresti essere in grado di fare come 30 resi e controlli int-val prima di recuperare il costo di sigjmp / longjmp. La maggior parte dei callstacks non va così in profondità soprattutto se non si va pesantemente alla ricorsione (e se lo si fa, si hanno problemi perf oltre al costo di resi + assegni).
PSkocik,

1
Se hai mallocato la memoria e poi hai lanciato, la memoria perderà per sempre. Inoltre setjmpè costoso, anche se non viene mai generato alcun errore, consumerà un bel po 'di tempo della CPU e spazio nello stack. Quando usi gcc per Windows, puoi scegliere tra diversi metodi di gestione delle eccezioni per C ++, uno dei quali si basa setjmpe rende il tuo codice fino al 30% più lento in pratica.
Mecki

7

Personalmente preferisco il primo approccio (restituendo un indicatore di errore).

Se necessario, il risultato restituito dovrebbe semplicemente indicare che si è verificato un errore, con un'altra funzione utilizzata per scoprire l'errore esatto.

Nel tuo esempio getSize () considererei che le dimensioni devono essere sempre zero o positive, quindi restituire un risultato negativo può indicare un errore, proprio come fanno le chiamate di sistema UNIX.

Non riesco a pensare a nessuna libreria che ho usato per il secondo approccio con un oggetto errore passato come puntatore. stdio, ecc. vanno tutti con un valore di ritorno.


1
Per la cronaca, una libreria che ho visto usare quest'ultimo approccio è l'API di programmazione Maya. È una libreria c ++ anziché C. È abbastanza incoerente nel modo in cui gestisce i suoi errori e talvolta l'errore viene passato come valore di ritorno e altre volte passa il risultato come riferimento.
Laserallan,

1
non dimenticare strtod, ok, l'ultimo argomento non è solo per indicare errori, ma lo fa anche.
Quinmars,

7

Quando scrivo programmi, durante l'inizializzazione, di solito eseguo un thread per la gestione degli errori e inizializzo una struttura speciale per gli errori, incluso un blocco. Quindi, quando rilevo un errore, tramite i valori di ritorno, inserisco le informazioni dall'eccezione nella struttura e invio un SIGIO al thread di gestione delle eccezioni, quindi vedo se non riesco a continuare l'esecuzione. Se non ci riesco, invio un SIGURG al thread delle eccezioni, che arresta il programma con garbo.


7

La restituzione del codice di errore è il solito approccio per la gestione degli errori in C.

Ma recentemente abbiamo sperimentato anche l'approccio del puntatore dell'errore in uscita.

Presenta alcuni vantaggi rispetto all'approccio del valore di ritorno:

  • È possibile utilizzare il valore restituito per scopi più significativi.

  • Dover scrivere quel parametro di errore ti ricorda di gestire l'errore o propagarlo. (Non dimenticare mai di controllare il valore di ritorno di fclose, vero?)

  • Se si utilizza un puntatore di errore, è possibile trasmetterlo mentre si chiamano le funzioni. Se una delle funzioni lo imposta, il valore non andrà perso.

  • Impostando un breakpoint di dati sulla variabile di errore, è possibile individuare dove si è verificato per primo l'errore. Impostando un breakpoint condizionale è possibile rilevare anche errori specifici.

  • Semplifica l'automatizzazione del controllo se gestisci tutti gli errori. La convenzione sul codice può costringere a chiamare il puntatore dell'errore come erre deve essere l'ultimo argomento. Quindi lo script può corrispondere alla stringa, err);quindi controlla se è seguito da if (*err. In pratica in pratica abbiamo creato una macro chiamata CER(check err return) e CEG(check err goto). Quindi non è necessario scriverlo sempre quando vogliamo solo tornare sull'errore e possiamo ridurre il disordine visivo.

Tuttavia, non tutte le funzioni nel nostro codice hanno questo parametro in uscita. Questa cosa del parametro in uscita viene utilizzata per i casi in cui normalmente si genera un'eccezione.


6

Ho fatto molta programmazione in C in passato. E ho davvero apprezzato il valore di ritorno del codice di errore. Ma ha diverse possibili insidie:

  • Numeri di errore duplicati, questo può essere risolto con un file degli errori globale.
  • Dimenticando di controllare il codice di errore, questo dovrebbe essere risolto con un cluebat e lunghe ore di debug. Ma alla fine imparerai (o saprai che qualcun altro eseguirà il debug).

2
il secondo problema può essere risolto mediante un livello di avviso del compilatore, un meccanismo di revisione del codice adeguato e strumenti di analisi del codice statico.
Ilya,

1
Puoi anche lavorare sul principio: se viene chiamata la funzione API e il valore restituito non viene controllato, c'è un bug.
Jonathan Leffler,

6

L'approccio UNIX è molto simile al tuo secondo suggerimento. Restituisce il risultato o un singolo valore "è andato storto". Ad esempio, open restituirà il descrittore di file in caso di successo o -1 in caso di errore. In caso di errore, imposta anche errnoun intero globale esterno per indicare quale errore si è verificato.

Per quello che vale, anche Cocoa ha adottato un approccio simile. Numerosi metodi restituiscono BOOL e accettano un NSError **parametro, in modo che in caso di errore impostino l'errore e restituiscano NO. Quindi la gestione degli errori è simile a:

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

che si trova tra le due opzioni :-).



5

Ecco un approccio che ritengo interessante, pur richiedendo un po 'di disciplina.

Ciò presuppone che una variabile di tipo handle sia l'istanza su cui operano tutte le funzioni API.

L'idea è che la struttura dietro l'handle memorizza l'errore precedente come una struttura con i dati necessari (codice, messaggio ...) e all'utente viene fornita una funzione che restituisce un puntatore a questo oggetto di errore. Ogni operazione aggiornerà l'oggetto appuntito in modo che l'utente possa verificarne lo stato senza nemmeno chiamare le funzioni. A differenza del modello errno, il codice di errore non è globale, il che rende l'approccio sicuro per i thread, purché ciascun handle sia utilizzato correttamente.

Esempio:

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;
}

4

Il primo approccio è migliore IMHO:

  • È più facile scrivere la funzione in quel modo. Quando si nota un errore nel mezzo della funzione, si restituisce semplicemente un valore di errore. Nel secondo approccio è necessario assegnare un valore di errore a uno dei parametri e quindi restituire qualcosa .... ma cosa restituiresti: non hai un valore corretto e non restituisci un valore di errore.
  • è più popolare, quindi sarà più facile da capire, mantenere

4

Preferisco decisamente la prima soluzione:

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

lo modificherei leggermente per:

int size;
MYAPIError rc;

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

Inoltre, non mescolerò mai il valore di ritorno legittimo con l'errore, anche se attualmente l'ambito della funzione ti consente di farlo, non sai mai in che modo verrà implementata la funzione in futuro.

E se stiamo già parlando della gestione degli errori, suggerirei goto Error;come codice di gestione degli errori, a meno che alcune undofunzioni non possano essere chiamate per gestire correttamente la gestione degli errori.


3

Quello che potresti fare invece di restituire il tuo errore, e quindi vietarti di restituire dati con la tua funzione, è usare un wrapper per il tuo tipo di reso:

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

Quindi, nella funzione chiamata:

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;
} 

Si noti che con il seguente metodo, il wrapper avrà la dimensione di MyType più un byte (sulla maggior parte dei compilatori), il che è abbastanza redditizio; e non dovrai inserire un altro argomento nello stack quando chiami la tua funzione ( returnedSizeo returnedErrorin entrambi i metodi che hai presentato).


3

Ecco un semplice programma per dimostrare i primi 2 proiettili della risposta di Nils Pipenbrinck qui .

I suoi primi 2 proiettili sono:

  • memorizza tutti i possibili stati di errore in un enum dattiloscritto e usalo nella tua libreria. Non solo restituire ints o peggio ancora, mescolare ints o enumerazioni diverse con codici di ritorno.

  • fornire una funzione che converte gli errori in qualcosa di leggibile dall'uomo. Può essere semplice Solo errore-enum in, const char * out.

Supponiamo di aver scritto un modulo chiamato mymodule. Innanzitutto, in mymodule.h, definisci i tuoi codici di errore basati su enum e scrivi alcune stringhe di errore che corrispondono a questi codici. Qui sto usando una matrice di stringhe C ( char *), che funziona bene solo se il tuo primo codice di errore basato su enum ha valore 0, e successivamente non manipoli i numeri. Se usi numeri di codice di errore con spazi vuoti o altri valori iniziali, dovrai semplicemente passare dall'uso di un array di stringhe C mappato (come faccio di seguito) all'utilizzo di una funzione che utilizza un'istruzione switch o if / else if statement mappare dai codici di errore enum alle stringhe C stampabili (cosa che non dimostrerò). La scelta è tua.

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 contiene la mia funzione di mapping per mappare dai codici di errore enum alle stringhe C stampabili:

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 contiene un programma di test per dimostrare la chiamata di alcune funzioni e la stampa di alcuni codici di errore da esse:

main.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;
}

Produzione:

Dimostrazione di codici di errore basati su enum in
codice err C (o C ++) da mymodule_func1 () = MYMODULE_ERROR_OK
codice err da mymodule_func2 () = MYMODULE_ERROR_INVARG
codice err da mymodule_func3 () = MYMODULE_ERROR_MYERROR

Riferimenti:

Puoi eseguire questo codice da solo qui: https://onlinegdb.com/ByEbKLupS .


2

Oltre a ciò che è stato detto, prima di restituire il codice di errore, attivare un'affermazione o una diagnostica simile quando viene restituito un errore, poiché renderà molto più facile la traccia. Il modo in cui lo faccio è avere un'asserzione personalizzata che viene ancora compilata al momento del rilascio ma viene attivata solo quando il software è in modalità diagnostica, con un'opzione per segnalare silenziosamente un file di registro o mettere in pausa sullo schermo.

Personalmente restituisco i codici di errore come numeri interi negativi con no_error come zero, ma ti lascia con il possibile bug seguente

if (MyFunc())
 DoSomething();

Un'alternativa è avere un errore sempre restituito come zero e utilizzare una funzione LastError () per fornire dettagli sull'errore effettivo.


2

Ho incontrato queste domande e risposte diverse volte e volevo contribuire con una risposta più completa. Penso che il modo migliore di pensare a questo sia come restituire errori al chiamante e cosa restituisci.

Come

Esistono 3 modi per restituire informazioni da una funzione:

  1. Valore di ritorno
  2. Argomenti fuori sede
  3. Out of Band, che include goto non locale (setjmp / longjmp), file o variabili con ambito globale, file system ecc.

Valore di ritorno

È possibile restituire solo il valore è un singolo oggetto, tuttavia, può essere un complesso arbitrario. Ecco un esempio di un errore che restituisce la funzione:

  enum error hold_my_beer();

Un vantaggio dei valori di ritorno è che consente il concatenamento delle chiamate per una gestione degli errori meno invasiva:

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

Ciò non riguarda solo la leggibilità, ma può anche consentire l'elaborazione di una matrice di tali puntatori a funzioni in modo uniforme.

Argomenti fuori sede

Puoi restituire di più tramite più di un oggetto tramite argomenti, ma le migliori pratiche suggeriscono di mantenere basso il numero totale di argomenti (diciamo <= 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);
}

Fuori banda

Con setjmp () si definisce un luogo e come si desidera gestire un valore int e si trasferisce il controllo a tale posizione tramite un longjmp (). Vedi l'utilizzo pratico di setjmp e longjmp in C .

Che cosa

  1. Indicatore
  2. Codice
  3. Oggetto
  4. Richiama

Indicatore

Un indicatore di errore indica solo che esiste un problema, ma nulla sulla natura di tale problema:

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

Questo è il modo meno potente per una funzione di comunicare lo stato di errore, tuttavia, perfetto se il chiamante non può comunque rispondere all'errore in modo graduato.

Codice

Un codice di errore indica al chiamante la natura del problema e può consentire una risposta adeguata (da quanto sopra). Può essere un valore restituito o simile all'esempio look_ma () sopra un argomento di errore.

Oggetto

Con un oggetto errore, il chiamante può essere informato su problemi complicati arbitrari. Ad esempio, un codice di errore e un messaggio leggibile umano adatto. Può anche informare il chiamante che più cose sono andate male o un errore per articolo durante l'elaborazione di una raccolta:

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]);
}

Invece di pre-allocare l'array di errori, puoi anche (ri) allocarlo dinamicamente secondo necessità, ovviamente.

Richiama

Il callback è il modo più potente per gestire gli errori, in quanto puoi dire alla funzione quale comportamento vorresti vedere accadere quando qualcosa va storto. Un argomento di callback può essere aggiunto a ciascuna funzione o se la personalizzazione è richiesta solo per istanza di una struttura come questa:

 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();

Un vantaggio interessante di un callback è che può essere invocato più volte, o in nessun caso in assenza di errori in cui non ci sono spese generali sulla via felice.

Vi è, tuttavia, un'inversione di controllo. Il codice chiamante non sa se il callback è stato invocato. Pertanto, può essere logico utilizzare anche un indicatore.


1

EDIT: se hai bisogno di accedere solo all'ultimo errore e non lavori in ambiente multithread.

Puoi restituire solo vero / falso (o qualche tipo di #define se lavori in C e non supporti le variabili bool) e disponi di un buffer degli errori globale che conterrà l'ultimo errore:

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
}

In effetti, ma non è C, potrebbe essere fornito dal sistema operativo o meno. Se stai lavorando su sistemi operativi in ​​tempo reale, ad esempio, non lo avrai.
Ilya,

1

Il secondo approccio consente al compilatore di produrre codice più ottimizzato, poiché quando l'indirizzo di una variabile viene passato a una funzione, il compilatore non può mantenere il suo valore nei registri durante le successive chiamate ad altre funzioni. Il codice di completamento di solito viene utilizzato una sola volta, subito dopo la chiamata, mentre i dati "reali" restituiti dalla chiamata possono essere utilizzati più spesso


1

Preferisco la gestione degli errori in C usando la seguente tecnica:

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);
}

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


1
Buona tecnica. Trovo anche più ordinato con goto'invece di ripetuti if'. Riferimenti: uno , due .
Ant_222

0

Inoltre le altre ottime risposte, ti suggerisco di provare a separare il flag di errore e il codice di errore al fine di salvare una linea su ogni chiamata, ovvero:

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

Quando hai un sacco di controllo degli errori, questa piccola semplificazione aiuta davvero.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.