Gestion des erreurs dans le code C

152

Que considérez-vous comme «meilleure pratique» en ce qui concerne la gestion des erreurs de manière cohérente dans une bibliothèque C.

J'ai pensé à deux façons:

Renvoyez toujours le code d'erreur. Une fonction typique ressemblerait à ceci:

MYAPI_ERROR getObjectSize(MYAPIHandle h, int* returnedSize);

Le toujours fournir une approche de pointeur d'erreur:

int getObjectSize(MYAPIHandle h, MYAPI_ERROR* returnedError);

Lors de l'utilisation de la première approche, il est possible d'écrire du code comme celui-ci où le contrôle de gestion des erreurs est directement placé sur l'appel de fonction:

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

Ce qui semble mieux que le code de gestion des erreurs ici.

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

Cependant, je pense que l'utilisation de la valeur de retour pour renvoyer des données rend le code plus lisible.Il est évident que quelque chose a été écrit dans la variable de taille dans le deuxième exemple.

Avez-vous des idées sur les raisons pour lesquelles je devrais préférer l'une de ces approches ou peut-être les mélanger ou utiliser autre chose? Je ne suis pas fan des états d'erreur globaux car cela a tendance à rendre l'utilisation multi-threadée de la bibliothèque beaucoup plus douloureuse.

EDIT: Des idées spécifiques au C ++ à ce sujet seraient également intéressantes à entendre tant qu'elles n'impliquent pas d'exceptions, car ce n'est pas une option pour moi pour le moment ...

Laserallan
la source
Je n'apprends C que depuis environ deux semaines, mais l'impression que j'ai eue est que les paramètres OUT sont la valeur de retour de facto pour la majorité des fonctions, car cela évite la surcharge de retour des structures par valeur et atténue la nécessité de désallouer la mémoire car la plupart des variables sont sur la pile. Donc, comme je n'utilise pas "return" pour la valeur réelle de la fonction, je suis libre de l'utiliser pour la gestion des erreurs la plupart du temps.
Joel Roberts le

Réponses:

74

J'aime l'erreur comme moyen de valeur de retour. Si vous concevez l'API et que vous souhaitez utiliser votre bibliothèque aussi facilement que possible, pensez à ces ajouts:

  • stockez tous les états d'erreur possibles dans une énumération typedef'ed et utilisez-la dans votre lib. Ne retournez pas seulement des entiers ou pire encore, mélangez des entiers ou différentes énumérations avec des codes de retour.

  • fournir une fonction qui convertit les erreurs en quelque chose de lisible par l'homme. Cela peut être simple. Juste erreur-enum in, const char * out.

  • Je sais que cette idée rend l'utilisation multithread un peu difficile, mais ce serait bien si le programmeur d'application peut définir un rappel d'erreur global. De cette façon, ils pourront mettre un point d'arrêt dans le rappel pendant les sessions de recherche de bogues.

J'espère que ça aide.

Nils Pipenbrinck
la source
5
Pourquoi dites-vous, "cette idée rend l'utilisation multi-thread un peu difficile." Quelle partie est rendue difficile par le multi-threading? Pouvez-vous donner un exemple rapide?
SayeedHussain
1
@crypticcoder Dit simplement: un rappel d'erreur global peut être invoqué dans n'importe quel contexte de thread. Si vous imprimez simplement l'erreur, vous ne rencontrerez aucun problème. Si vous essayez de corriger des problèmes, vous devrez trouver quel thread appelant a causé l'erreur, ce qui complique les choses.
Nils Pipenbrinck
9
Que faire si vous souhaitez communiquer plus de détails sur l'erreur? Par exemple, vous avez une erreur d'analyseur et souhaitez fournir le numéro de ligne et la colonne de l'erreur de syntaxe et un moyen de tout imprimer correctement.
panzi
1
@panzi alors vous devez évidemment renvoyer un struct (ou utiliser un pointeur out si struct est vraiment grand) et avoir une fonction pour formater le struct sous forme de chaîne.
Ailier Sendon du
Je montre vos 2 premières puces dans le code ici: stackoverflow.com/questions/385975/error-handling-in-c-code/...
Gabriel Staples
92

J'ai utilisé les deux approches, et elles ont toutes les deux bien fonctionné pour moi. Quel que soit celui que j'utilise, j'essaie toujours d'appliquer ce principe:

Si les seules erreurs possibles sont des erreurs de programmeur, ne renvoyez pas de code d'erreur, utilisez des assertions à l'intérieur de la fonction.

Une assertion qui valide les entrées communique clairement ce que la fonction attend, tandis qu'une trop grande vérification des erreurs peut obscurcir la logique du programme. Décider quoi faire pour tous les différents cas d'erreur peut vraiment compliquer la conception. Pourquoi comprendre comment functionX doit gérer un pointeur nul si vous pouvez à la place insister pour que le programmeur n'en passe jamais un?

AShelly
la source
1
Vous avez un exemple d'assertions en C? (Je suis très vert à C)
thomthom
C'est généralement aussi simple que assert(X)où X est une instruction C valide que vous voulez être vraie. voir stackoverflow.com/q/1571340/10396 .
AShelly
14
Ugh, n'utilisez absolument jamais d'assertions dans le code de la bibliothèque ! De plus, ne mélangez pas différents styles de gestion des erreurs dans un seul morceau de code comme d'autres l'ont fait…
mirabilos
10
Je suis certainement d'accord pour ne pas mélanger les styles. Je suis curieux de connaître votre raisonnement sur les affirmations. Si ma documentation de fonction dit "l'argument X ne doit pas être NULL" ou "Y doit être membre de cette énumération", qu'est-ce qui ne va pas avec assert(X!=NULL);ou assert(Y<enumtype_MAX);? Voir cette réponse sur les programmeurs et la question à laquelle elle renvoie pour plus de détails sur les raisons pour lesquelles je pense que c'est la bonne voie à suivre.
AShelly
8
@AShelly Le problème avec les affirmations qu'ils ne sont généralement pas présents dans les versions de version.
Calmarius
29

Il existe un bel ensemble de diapositives du CERT de la CMU avec des recommandations sur le moment où utiliser chacune des techniques courantes de gestion des erreurs C (et C ++). L'une des meilleures diapositives est cet arbre de décision:

Arbre de décision de gestion des erreurs

Je changerais personnellement deux choses à propos de ce flowcart.

Tout d'abord, je préciserais que parfois les objets doivent utiliser des valeurs de retour pour indiquer des erreurs. Si une fonction extrait uniquement les données d'un objet mais ne mute pas l'objet, alors l'intégrité de l'objet lui-même n'est pas menacée et l'indication des erreurs à l'aide d'une valeur de retour est plus appropriée.

Deuxièmement, il n'est pas toujours approprié d'utiliser des exceptions en C ++. Les exceptions sont bonnes car elles peuvent réduire la quantité de code source consacrée à la gestion des erreurs, elles n'affectent généralement pas les signatures de fonction et elles sont très flexibles quant aux données qu'elles peuvent transmettre à la pile d'appels. D'un autre côté, les exceptions peuvent ne pas être le bon choix pour plusieurs raisons:

  1. Les exceptions C ++ ont une sémantique très particulière. Si vous ne voulez pas de cette sémantique, les exceptions C ++ sont un mauvais choix. Une exception doit être traitée immédiatement après avoir été lancée et la conception favorise le cas où une erreur aura besoin de dérouler la pile d'appels de quelques niveaux.

  2. Les fonctions C ++ qui lèvent des exceptions ne peuvent pas être encapsulées ultérieurement pour ne pas lever d'exceptions, du moins pas sans payer le coût total des exceptions de toute façon. Les fonctions qui renvoient des codes d'erreur peuvent être encapsulées pour lever des exceptions C ++, ce qui les rend plus flexibles. C ++ newobtient ce droit en fournissant une variante non lanceuse.

  3. Les exceptions C ++ sont relativement chères, mais cet inconvénient est surtout exagéré pour les programmes utilisant judicieusement les exceptions. Un programme ne devrait tout simplement pas lancer d'exceptions sur un chemin de code où les performances sont un problème. La vitesse à laquelle votre programme peut signaler une erreur et se terminer n'a pas vraiment d'importance.

  4. Parfois, les exceptions C ++ ne sont pas disponibles. Soit ils ne sont littéralement pas disponibles dans l'implémentation C ++, soit les directives de code les interdisent.


Puisque la question originale portait sur un contexte multithread, je pense que la technique de l'indicateur d'erreur local (ce qui est décrit dans la réponse de SirDarius ) était sous-estimée dans les réponses originales. Il est threadsafe, ne force pas l'erreur à être immédiatement traitée par l'appelant et peut regrouper des données arbitraires décrivant l'erreur. L'inconvénient est qu'il doit être détenu par un objet (ou, je suppose, associé d'une manière externe) et est sans doute plus facile à ignorer qu'un code de retour.

Praxéolitique
la source
5
Vous remarquerez peut-être que les normes de codage C ++ de Google indiquent toujours que nous n'utilisons pas d'exceptions C ++.
Jonathan Leffler
19

J'utilise la première approche chaque fois que je crée une bibliothèque. Il y a plusieurs avantages à utiliser une énumération typedef'ed comme code de retour.

  • Si la fonction renvoie une sortie plus compliquée telle qu'un tableau et sa longueur, vous n'avez pas besoin de créer des structures arbitraires à renvoyer.

    rc = func(..., int **return_array, size_t *array_length);
  • Il permet une gestion des erreurs simple et standardisée.

    if ((rc = func(...)) != API_SUCCESS) {
       /* Error Handling */
    }
  • Il permet une gestion simple des erreurs dans la fonction de bibliothèque.

    /* Check for valid arguments */
    if (NULL == return_array || NULL == array_length)
        return API_INVALID_ARGS;
  • L'utilisation d'une énumération typedef'ed permet également au nom d'énumération d'être visible dans le débogueur. Cela permet un débogage plus facile sans avoir à consulter constamment un fichier d'en-tête. Avoir une fonction pour traduire cette énumération en une chaîne est également utile.

La question la plus importante, quelle que soit l'approche utilisée, est d'être cohérent. Cela s'applique à la dénomination des fonctions et des arguments, à l'ordre des arguments et à la gestion des erreurs.

Jeffrey Cohen
la source
9

Utilisez 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
la source
2
Le deuxième bloc de code est basé sur une version antérieure du code sur la page de Francesco Nidito référencée en haut de la réponse. Le ETRYcode a été révisé depuis que cette réponse a été écrite.
Jonathan Leffler
2
Setjmp est une horrible stratégie de gestion des erreurs. C'est cher, sujet aux erreurs (avec des locales modifiées non volatiles ne conservant pas leurs valeurs modifiées et tout) et des fuites de ressources si vous en allouez entre les appels setjmp et longjmp. Vous devriez pouvoir faire comme 30 retours et vérifications int-val avant de récupérer le coût de sigjmp / longjmp. La plupart des callstacks ne vont pas aussi loin, surtout si vous ne faites pas beaucoup de récursions (et si vous le faites, vous avez des problèmes de performance autres que le coût des retours + chèques).
PSkocik
1
Si vous malmenez la mémoire puis la jetez, la mémoire perdra à jamais. C'est aussi setjmpcher, même si aucune erreur n'est jamais générée, cela consommera pas mal de temps CPU et d'espace de pile. Lorsque vous utilisez gcc pour Windows, vous pouvez choisir entre différentes méthodes de gestion des exceptions pour C ++, l'une d'elles est basée sur setjmpet cela rend votre code jusqu'à 30% plus lent dans la pratique.
Mecki
7

Je préfère personnellement la première approche (renvoyant un indicateur d'erreur).

Si nécessaire, le résultat renvoyé doit simplement indiquer qu'une erreur s'est produite, une autre fonction étant utilisée pour trouver l'erreur exacte.

Dans votre exemple getSize (), je considérerais que les tailles doivent toujours être nulles ou positives, donc renvoyer un résultat négatif peut indiquer une erreur, un peu comme le font les appels système UNIX.

Je ne peux penser à aucune bibliothèque que j'ai utilisée qui opte pour cette dernière approche avec un objet d'erreur passé en tant que pointeur. stdio, etc tous vont avec une valeur de retour.

Alnitak
la source
1
Pour mémoire, une bibliothèque que j'ai vue utiliser cette dernière approche est l'API de programmation Maya. C'est une bibliothèque C ++ plutôt que C cependant. Il est assez incohérent dans la façon dont il gère ses erreurs et parfois l'erreur est passée comme valeur de retour et d'autres fois, il transmet le résultat comme référence.
Laserallan
1
n'oubliez pas strtod, ok, le dernier argument n'est pas seulement pour indiquer les erreurs, mais il le fait aussi.
quinmars
7

Lorsque j'écris des programmes, lors de l'initialisation, je lance généralement un thread pour la gestion des erreurs et j'initialise une structure spéciale pour les erreurs, y compris un verrou. Ensuite, lorsque je détecte une erreur, via des valeurs de retour, j'entre les informations de l'exception dans la structure et j'envoie un SIGIO au thread de gestion des exceptions, puis je vois si je ne peux pas continuer l'exécution. Si je ne peux pas, j'envoie un SIGURG au thread d'exception, qui arrête le programme gracieusement.

Henri
la source
7

Le renvoi du code d'erreur est l'approche habituelle pour la gestion des erreurs en C.

Mais récemment, nous avons également expérimenté l'approche du pointeur d'erreur sortant.

Elle présente certains avantages par rapport à l'approche de la valeur de retour:

  • Vous pouvez utiliser la valeur de retour à des fins plus significatives.

  • Le fait de devoir écrire ce paramètre d'erreur vous rappelle de gérer l'erreur ou de la propager. (Vous n'oublie jamais de vérifier la valeur de retour de fclose, n'est-ce pas?)

  • Si vous utilisez un pointeur d'erreur, vous pouvez le transmettre lorsque vous appelez des fonctions. Si l'une des fonctions le définit, la valeur ne sera pas perdue.

  • En définissant un point d'arrêt de données sur la variable d'erreur, vous pouvez identifier où l'erreur s'est produite en premier. En définissant un point d'arrêt conditionnel, vous pouvez également détecter des erreurs spécifiques.

  • Cela facilite l'automatisation du contrôle si vous gérez toutes les erreurs. La convention de code peut vous forcer à appeler votre pointeur d'erreur en tant que erret ce doit être le dernier argument. Ainsi, le script peut correspondre à la chaîne, err);puis vérifiez si elle est suivie de if (*err. En fait, dans la pratique, nous avons créé une macro appelée CER(check err return) et CEG(check err goto). Vous n'avez donc pas besoin de le taper toujours lorsque nous voulons simplement revenir sur une erreur et que vous pouvez réduire l'encombrement visuel.

Cependant, toutes les fonctions de notre code n'ont pas ce paramètre sortant. Ce paramètre sortant est utilisé dans les cas où vous lèveriez normalement une exception.

Calmaire
la source
6

J'ai fait beaucoup de programmation C dans le passé. Et j'ai vraiment apprécié la valeur de retour du code d'erreur. Mais il a plusieurs écueils possibles:

  • Numéros d'erreur en double, cela peut être résolu avec un fichier global errors.h.
  • Oublier de vérifier le code d'erreur, cela devrait être résolu avec un cluebat et de longues heures de débogage. Mais à la fin, vous apprendrez (ou vous saurez que quelqu'un d'autre fera le débogage).
Toon Krijthe
la source
2
Le deuxième problème peut être résolu par un niveau d'avertissement approprié du compilateur, un mécanisme de révision de code approprié et par des outils d'analyse de code statique.
Ilya
1
Vous pouvez également travailler sur le principe: si la fonction API est appelée et que la valeur de retour n'est pas vérifiée, il y a un bug.
Jonathan Leffler
6

L'approche UNIX est très similaire à votre deuxième suggestion. Renvoie soit le résultat, soit une seule valeur "il s'est mal passé". Par exemple, open renverra le descripteur de fichier en cas de succès ou -1 en cas d'échec. En cas d'échec, il définit également errnoun entier global externe pour indiquer l' échec qui s'est produit.

Pour ce que ça vaut, Cocoa a également adopté une approche similaire. Un certain nombre de méthodes renvoient BOOL et prennent un NSError **paramètre, de sorte qu'en cas d'échec, elles définissent l'erreur et renvoient NON. Ensuite, la gestion des erreurs ressemble à:

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

qui est quelque part entre vos deux options :-).


la source
5

Voici une approche que je trouve intéressante, tout en exigeant une certaine discipline.

Cela suppose qu'une variable de type handle est l'instance sur laquelle fonctionnent toutes les fonctions de l'API.

L'idée est que la structure derrière le handle stocke l'erreur précédente en tant que structure avec les données nécessaires (code, message ...), et l'utilisateur reçoit une fonction qui renvoie un pointeur sur cet objet d'erreur. Chaque opération mettra à jour l'objet pointé afin que l'utilisateur puisse vérifier son état sans même appeler de fonctions. Contrairement au modèle errno, le code d'erreur n'est pas global, ce qui rend l'approche thread-safe, tant que chaque handle est correctement utilisé.

Exemple:

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
la source
4

La première approche est meilleure à mon humble avis:

  • Il est plus facile d'écrire une fonction de cette façon. Lorsque vous remarquez une erreur au milieu de la fonction, vous renvoyez simplement une valeur d'erreur. Dans la deuxième approche, vous devez attribuer une valeur d'erreur à l'un des paramètres, puis renvoyer quelque chose ... mais que retourneriez-vous - vous n'avez pas la valeur correcte et vous ne renvoyez pas la valeur d'erreur.
  • il est plus populaire donc il sera plus facile à comprendre, à maintenir
Klesk
la source
4

Je préfère définitivement la première solution:

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

je le modifierais légèrement, pour:

int size;
MYAPIError rc;

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

De plus, je ne mélangerai jamais une valeur de retour légitime avec une erreur, même si actuellement, la portée de la fonction vous permettant de le faire, vous ne savez jamais dans quelle direction la mise en œuvre de la fonction se déroulera à l'avenir.

Et si nous parlons déjà de gestion des erreurs, je suggérerais goto Error;comme code de gestion des erreurs, à moins qu'une undofonction ne puisse être appelée pour gérer correctement la gestion des erreurs.

Il y a
la source
3

Ce que vous pourriez faire au lieu de renvoyer votre erreur, et donc vous interdire de renvoyer des données avec votre fonction, est d'utiliser un wrapper pour votre type de retour:

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

Ensuite, dans la fonction appelée:

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

Veuillez noter qu'avec la méthode suivante, le wrapper aura la taille de MyType plus un octet (sur la plupart des compilateurs), ce qui est assez rentable; et vous n'aurez pas à pousser un autre argument sur la pile lorsque vous appelez votre fonction ( returnedSizeou returnedErrordans les deux méthodes que vous avez présentées).

Antoine C.
la source
3

Voici un programme simple pour démontrer les 2 premières balles de la réponse de Nils Pipenbrinck ici .

Ses 2 premières balles sont:

  • stockez tous les états d'erreur possibles dans une énumération typedef'ed et utilisez-la dans votre lib. Ne retournez pas seulement des entiers ou pire encore, mélangez des entiers ou différentes énumérations avec des codes de retour.

  • fournir une fonction qui convertit les erreurs en quelque chose de lisible par l'homme. Cela peut être simple. Juste erreur-enum in, const char * out.

Supposons que vous ayez écrit un module nommé mymodule. Tout d'abord, dans mymodule.h, vous définissez vos codes d'erreur basés sur l'énumération et vous écrivez des chaînes d'erreur qui correspondent à ces codes. Ici, j'utilise un tableau de chaînes C ( char *), qui ne fonctionne bien que si votre premier code d'erreur basé sur une énumération a la valeur 0, et vous ne manipulez pas les nombres par la suite. Si vous utilisez des numéros de code d'erreur avec des espaces ou d'autres valeurs de départ, vous devrez simplement passer de l'utilisation d'un tableau de chaînes C mappé (comme je le fais ci-dessous) à l'utilisation d'une fonction qui utilise une instruction switch ou if / else if instructions pour mapper des codes d'erreur d'énumération aux chaînes C imprimables (que je ne démontre pas). Le choix t'appartient.

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 contient ma fonction de mappage pour mapper des codes d'erreur d'énumération aux chaînes C imprimables:

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 contient un programme de test pour démontrer l'appel de certaines fonctions et l'impression de certains codes d'erreur à partir d'elles:

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

Production:

Démonstration des codes d'erreur basés sur une énumération dans le code d'erreur C (ou C ++)
de mymodule_func1 () = MYMODULE_ERROR_OK
code d'erreur de mymodule_func2 () = MYMODULE_ERROR_INVARG
code d'erreur de mymodule_func3 () = MYMODULE_ERROR_MYERROR

Références:

Vous pouvez exécuter ce code vous-même ici: https://onlinegdb.com/ByEbKLupS .

Gabriel Staples
la source
2

En plus de ce qui a été dit, avant de renvoyer votre code d'erreur, lancez une assertion ou un diagnostic similaire lorsqu'une erreur est renvoyée, car cela rendra le traçage beaucoup plus facile. La façon dont je fais cela est d'avoir une assertion personnalisée qui est toujours compilée lors de la publication mais qui n'est déclenchée que lorsque le logiciel est en mode de diagnostic, avec une option pour signaler silencieusement à un fichier journal ou mettre en pause à l'écran.

Je renvoie personnellement les codes d'erreur sous forme d'entiers négatifs avec no_error comme zéro, mais cela vous laisse avec le bogue suivant possible

if (MyFunc())
 DoSomething();

Une alternative est d'avoir un échec toujours renvoyé comme zéro et d'utiliser une fonction LastError () pour fournir des détails sur l'erreur réelle.

SmacL
la source
2

J'ai rencontré cette question à plusieurs reprises et je voulais apporter une réponse plus complète. Je pense que la meilleure façon d'y penser est de savoir comment renvoyer les erreurs à l'appelant et ce que vous renvoyez.

Comment

Il existe 3 façons de renvoyer des informations à partir d'une fonction:

  1. Valeur de retour
  2. Argument (s) de sortie
  3. Hors bande, qui inclut goto non local (setjmp / longjmp), fichier ou variables de portée globale, système de fichiers, etc.

Valeur de retour

Vous ne pouvez renvoyer la valeur que dans un seul objet, cependant, cela peut être un complexe arbitraire. Voici un exemple d'une fonction de renvoi d'erreur:

  enum error hold_my_beer();

L'un des avantages des valeurs de retour est qu'elles permettent le chaînage des appels pour une gestion des erreurs moins intrusive:

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

Cela ne concerne pas seulement la lisibilité, mais peut également permettre de traiter un tableau de ces pointeurs de fonction de manière uniforme.

Argument (s) de sortie

Vous pouvez en renvoyer plus via plus d'un objet via des arguments, mais les meilleures pratiques suggèrent de garder le nombre total d'arguments faible (par exemple, <= 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);
}

Hors bande

Avec setjmp (), vous définissez un lieu et comment vous voulez gérer une valeur int, et vous transférez le contrôle vers cet emplacement via un longjmp (). Voir Utilisation pratique de setjmp et longjmp en C .

Quoi

  1. Indicateur
  2. Code
  3. Objet
  4. Rappeler

Indicateur

Un indicateur d'erreur vous indique seulement qu'il y a un problème mais rien sur la nature dudit problème:

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

C'est le moyen le moins puissant pour une fonction de communiquer l'état d'erreur, cependant, parfait si l'appelant ne peut de toute façon pas répondre à l'erreur de manière graduée.

Code

Un code d'erreur informe l'appelant de la nature du problème et peut permettre une réponse appropriée (à partir de ce qui précède). Cela peut être une valeur de retour, ou comme l'exemple look_ma () au-dessus d'un argument d'erreur.

Objet

Avec un objet d'erreur, l'appelant peut être informé de problèmes complexes arbitraires. Par exemple, un code d'erreur et un message lisible par l'homme approprié. Il peut également informer l'appelant que plusieurs choses se sont mal passées, ou une erreur par élément lors du traitement d'une collection:

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

Au lieu de pré-allouer le tableau d'erreurs, vous pouvez également (ré) allouer dynamiquement au besoin bien sûr.

Rappeler

Le rappel est le moyen le plus puissant de gérer les erreurs, car vous pouvez indiquer à la fonction quel comportement vous aimeriez voir se produire en cas de problème. Un argument de rappel peut être ajouté à chaque fonction, ou si la personnalisation n'est requise que par instance d'une structure comme celle-ci:

 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 avantage intéressant d'un rappel est qu'il peut être invoqué plusieurs fois, ou pas du tout en l'absence d'erreurs dans lesquelles il n'y a pas de surcharge sur le chemin heureux.

Il y a cependant une inversion de contrôle. Le code appelant ne sait pas si le rappel a été appelé. En tant que tel, il peut être judicieux d'utiliser également un indicateur.

Allan Wind
la source
1

EDIT: Si vous avez besoin d'accéder uniquement à la dernière erreur et que vous ne travaillez pas dans un environnement multithread.

Vous pouvez renvoyer uniquement vrai / faux (ou une sorte de #define si vous travaillez en C et ne supportez pas les variables booléennes), et avoir un tampon d'erreur global qui contiendra la dernière erreur:

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
la source
En effet, mais ce n'est pas C, il peut être fourni par le système d'exploitation ou non.Si vous travaillez sur des systèmes d'exploitation en temps réel par exemple, vous ne l'auriez pas.
Ilya
1

La deuxième approche permet au compilateur de produire du code plus optimisé, car lorsque l'adresse d'une variable est passée à une fonction, le compilateur ne peut pas conserver sa valeur dans le ou les registres lors des appels suivants à d'autres fonctions. Le code d'achèvement n'est généralement utilisé qu'une seule fois, juste après l'appel, alors que les données "réelles" renvoyées par l'appel peuvent être utilisées plus souvent

Dmityugov
la source
1

Je préfère la gestion des erreurs en C en utilisant la technique suivante:

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

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

Nitin Kunal
la source
1
Bonne technique. Je trouve encore plus soigné avec gotodes s au lieu de répétés if. Références: un , deux .
Ant_222
0

En plus des autres bonnes réponses, je vous suggère d'essayer de séparer l'indicateur d'erreur et le code d'erreur afin de sauvegarder une ligne à chaque appel, c'est-à-dire:

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

Lorsque vous avez beaucoup de vérification des erreurs, cette petite simplification est vraiment utile.

Ant_222
la source