Que considérez-vous comme la "meilleure pratique" en matière de gestion des erreurs de manière cohérente dans une bibliothèque C.
J'ai pensé à deux façons:
Toujours retourner le code d'erreur. Une fonction typique ressemblerait à ceci:
MYAPI_ERROR getObjectSize(MYAPIHandle h, int* returnedSize);
L'approche toujours fournir un pointeur d'erreur:
int getObjectSize(MYAPIHandle h, MYAPI_ERROR* returnedError);
Lorsque vous utilisez la première approche, il est possible d'écrire un code comme celui-ci où le contrôle de gestion des erreurs est directement placé sur l'appel de la fonction:
int size;
if(getObjectSize(h, &size) != MYAPI_SUCCESS) {
// Error handling
}
Ce qui est meilleur 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 une idée de la raison pour laquelle je devrais préférer l'une de ces approches ou peut-être les combiner ou utiliser autre chose? Je ne suis pas un partisan des états d'erreur globaux car cela tend à rendre l'utilisation de la bibliothèque multithread plus pénible.
EDIT: Des idées spécifiques à C++ sur ce sujet seraient également intéressantes à entendre tant qu’elles ne comportent pas d’exceptions, ce n’est pas une option pour moi pour le moment ...
J'aime l'erreur en tant que valeur de retour. Si vous concevez l’API et que vous souhaitez utiliser votre bibliothèque aussi simple que possible, pensez à ces ajouts:
stocke tous les états d'erreur possibles dans un enum typedef'ed et l'utilise dans votre lib. Ne renvoyez pas simplement des entrées ou, pire encore, mélangez des entrées ou des énumérations différentes avec des codes de retour.
fournir une fonction qui convertit les erreurs en quelque chose de lisible par l'homme. 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.
J'ai utilisé les deux approches, et les deux ont bien fonctionné pour moi. Quel que soit le modèle que j'utilise, j'essaie toujours d'appliquer ce principe:
Si les seules erreurs possibles sont des erreurs de programmation, ne retournez pas de code d'erreur, utilisez des assertions dans la fonction.
Une assertion qui valide les entrées communique clairement les attentes de la fonction, tandis qu'une vérification d'erreur trop importante peut masquer la logique du programme. Décider quoi faire pour tous les cas d'erreur variés peut vraiment compliquer la conception. Pourquoi comprendre comment functionX devrait gérer un pointeur null si vous pouvez plutôt insister pour que le programmeur ne le passe jamais?
Il y a un joli ensemble de diapositives du CERT de la CMU avec des recommandations pour savoir quand utiliser chacune des techniques courantes de traitement des erreurs C (et C++). Une des meilleures diapositives est cet arbre de décision:
Personnellement, je voudrais changer deux choses à propos de cette FlowCart.
Tout d’abord, j’aimerais préciser que les objets doivent parfois utiliser des valeurs de retour pour indiquer les erreurs. Si une fonction extrait uniquement les données d'un objet sans muter l'objet, l'intégrité de l'objet lui-même n'est pas menacée et l'indication d'erreurs à l'aide d'une valeur de retour est plus appropriée.
Deuxièmement, il n'est pas approprié toujours 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 sont très flexibles en ce qui concerne les données qu'elles peuvent transmettre à la pile d'appels. D'autre part, les exceptions pourraient ne pas être le bon choix pour plusieurs raisons:
Les exceptions C++ ont une sémantique très particulière. Si vous ne voulez pas de sémantique, les exceptions C++ sont un mauvais choix. Une exception doit être traitée immédiatement après avoir été levée et la conception favorise le cas où une erreur devra dérouler le callstack sur plusieurs niveaux.
Les fonctions C++ qui génèrent des exceptions ne peuvent pas être encapsulées ultérieurement pour ne pas déclencher des exceptions, du moins sans payer de toute façon le coût total des exceptions. Les fonctions qui renvoient des codes d'erreur peuvent être encapsulées pour générer des exceptions C++, ce qui les rend plus flexibles. La variable new
de C++ obtient ce droit en fournissant une variante non exécutable.
Les exceptions C++ sont relativement coûteuses, mais cet inconvénient est généralement exagéré pour les programmes qui utilisent judicieusement les exceptions. Un programme ne devrait tout simplement pas jeter d'exceptions sur un chemin de code lorsque la performance est une préoccupation. Peu importe la rapidité avec laquelle votre programme peut signaler une erreur et quitter.
Parfois, les exceptions C++ ne sont pas disponibles. Soit ils ne sont littéralement pas disponibles dans une implémentation C++, soit les directives de code les interdisent.
Comme la question initiale concernait un contexte multithread, je pense que la technique de l'indicateur d'erreur local (décrite dans SirDarius / s's answer ) a été sous-estimée dans les réponses originales. C'est threadsafe, ne force pas l'appelant à traiter immédiatement l'erreur, et peut rassembler des données arbitraires décrivant l'erreur. L'inconvénient est qu'il doit être tenu par un objet (ou associé d'une manière ou d'une autre à l'extérieur) et est sans doute plus facile à ignorer qu'un code de retour.
J'utilise la première approche chaque fois que je crée une bibliothèque. Il existe plusieurs avantages à utiliser une énumération typedef'ed comme code de retour.
Si la fonction renvoie une sortie plus complexe telle qu'un tableau et que sa longueur est inutile, vous n'avez pas besoin de créer de structures arbitraires pour la retourner.
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 */
}
Cela 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 que le nom de l'énumération soit visible dans le débogueur. Cela facilite le débogage sans avoir à consulter en permanence un fichier d’en-tête. Avoir une fonction pour traduire cette énumération en chaîne est également utile.
Le problème le plus important, quelle que soit l'approche utilisée, est d'être cohérent. Ceci s’applique à la dénomination des fonctions et des arguments, au classement des arguments et au traitement des erreurs.
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;
}
Lorsque j'écris des programmes, lors de l'initialisation, je désactive habituellement un thread pour la gestion des erreurs et initialise une structure spéciale pour les erreurs, y compris un verrou. Ensuite, lorsque je détecte une erreur dans les valeurs de retour, j'entre les informations de l'exception dans la structure et envoie un SIGIO au thread de traitement des exceptions, puis je vois si je ne peux pas continuer à exécuter. Si je ne peux pas, j'envoie un SIGURG au thread d'exception, qui arrête le programme normalement.
Personnellement, je préfère la première approche (retourner un indicateur d'erreur).
Le cas échéant, le résultat renvoyé doit simplement indiquer qu'une erreur s'est produite, une autre fonction étant utilisée pour rechercher l'erreur exacte.
Dans votre exemple getSize (), les tailles doivent toujours être nulles ou positives. Par conséquent, le résultat négatif peut indiquer une erreur, comme le font les appels système UNIX.
Je ne peux penser à aucune bibliothèque que j'ai utilisée qui soit compatible avec cette dernière approche avec un objet d'erreur transmis en tant que pointeur. stdio
, etc vont tous avec une valeur de retour.
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 sortante.
Il présente certains avantages par rapport à l'approche de la valeur de retour:
Vous pouvez utiliser la valeur de retour à des fins plus significatives.
Devoir écrire ce paramètre d'erreur vous rappelle de gérer l'erreur ou de la propager. (Vous n'oubliez 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 intercepter des erreurs spécifiques.
Il est plus facile d’automatiser le contrôle si vous gérez toutes les erreurs. La convention de code peut vous obliger à appeler votre pointeur d'erreur sous la forme err
et il doit s'agir du dernier argument. Donc, le script peut correspondre à la chaîne err);
puis vérifier si elle est suivie par if (*err
. En pratique, nous avons créé une macro appelée CER
(check err return) et CEG
(check err goto). Vous n’avez donc pas besoin de taper toujours lorsque nous voulons simplement renvoyer une erreur et réduire ainsi l’encombrement visuel.
Toutes les fonctions de notre code ne possèdent pas ce paramètre sortant cependant . Ce paramètre sortant est utilisé dans les cas où vous lèveriez normalement une exception.
J'ai beaucoup travaillé en programmation C dans le passé. Et j'ai vraiment apprécié la valeur de retour du code d'erreur. Mais il y a plusieurs pièges possibles:
L'approche UNIX ressemble le plus à votre deuxième suggestion. Renvoie le résultat ou une seule valeur "il a mal tourné". Par exemple, open retournera le descripteur de fichier en cas de succès ou -1 en cas d'échec. En cas d'échec, il définit également errno
, un entier global externe pour indiquer lequel échec s'est produit.
Pour ce que cela vaut, Cocoa a également adopté une approche similaire. Un certain nombre de méthodes renvoient BOOL et prennent un paramètre NSError **
. Ainsi, en cas d'erreur, elles définissent l'erreur et renvoient NO. Ensuite, la gestion des erreurs ressemble à ceci:
NSError *error = nil;
if ([myThing doThingError: &error] == NO)
{
// error handling
}
ce qui est quelque part entre vos deux options :-).
Voici une approche qui me semble intéressante, tout en exigeant une certaine discipline.
Cela suppose qu'une variable de type descripteur est l'instance sur laquelle toutes les fonctions de l'API sont exploitées.
L'idée est que la structure derrière le descripteur stocke l'erreur précédente sous la forme d'une structure avec les données nécessaires (code, message ...), et l'utilisateur dispose d'une fonction qui renvoie un pointeur sur cet objet d'erreur. Chaque opération met à jour l'objet pointé afin que l'utilisateur puisse vérifier son statut 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 descripteur est utilisé correctement.
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;
}
Je réfléchissais aussi récemment à ce problème et ai écrit des macros pour C qui simulent la sémantique try-catch-finally en utilisant des valeurs de retour purement locales. J'espère que vous le trouverez utile.
Première approche est mieux à mon humble avis:
J'ai rencontré ce Q & R à plusieurs reprises et je voulais apporter une réponse plus complète. Je pense que la meilleure façon de penser à cela est comment renvoyer les erreurs à l'appelant, et ce que vous renvoyez.
Il existe 3 façons de renvoyer des informations à partir d'une fonction:
Vous pouvez uniquement renvoyer une valeur correspondant à un objet unique, mais il peut s'agir d'un complexe arbitraire. Voici un exemple de fonction renvoyant une erreur:
enum error hold_my_beer();
L'un des avantages des valeurs de retour est qu'elles permettent de chaîner des appels pour une gestion des erreurs moins intrusive:
!hold_my_beer() &&
!hold_my_cigarette() &&
!hold_my_pants() ||
abort();
Ceci ne concerne pas seulement la lisibilité, mais peut également permettre de traiter un tableau de tels pointeurs de fonction de manière uniforme.
Vous pouvez retourner plus d’objets via plus d’un objet via des arguments, mais il est préférable de limiter le nombre total d’arguments (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);
}
Avec setjmp (), vous définissez un emplacement et comment vous souhaitez gérer une valeur int, et vous transférez le contrôle à cet emplacement via un longjmp (). Voir Utilisation pratique de setjmp et longjmp en C .
Un indicateur d'erreur indique uniquement 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, mais il est parfait si l’appelant ne peut pas répondre à l’erreur de manière graduelle.
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). Il peut s'agir d'une valeur de retour ou, comme dans l'exemple de look_ma (), d'un argument d'erreur.
Avec un objet d'erreur, l'appelant peut être informé de problèmes complexes et arbitraires. Par exemple, un code d'erreur et un message lisible par l'homme approprié. Il peut également informer l'appelant que plusieurs problèmes se sont produits, 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 aussi (ré) l’allouer dynamiquement selon vos besoins.
Le rappel est le moyen le plus efficace pour gérer les erreurs, car vous pouvez indiquer à la fonction le comportement que vous souhaitez 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 appelé plusieurs fois, voire 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 du 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.
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 la valeur de retour légitime avec une erreur, même si la portée de la fonction vous permet de le faire, vous ne savez jamais de quelle manière l'implémentation de la fonction ira dans le futur.
Et si nous parlons déjà de la gestion des erreurs, je suggérerais goto Error;
comme code de gestion des erreurs, sauf si une fonction undo
peut être appelée pour gérer correctement la gestion des erreurs.
En plus de ce qui a été dit, avant de renvoyer votre code d'erreur, déclenchez une assertion ou un diagnostic similaire lorsqu'une erreur est renvoyée, car cela facilitera beaucoup le traçage. Pour ce faire, je crée une assertion personnalisée qui est toujours compilée au moment de la publication mais qui ne se déclenche que lorsque le logiciel est en mode de diagnostic, avec la possibilité de créer un rapport en mode silencieux dans un fichier journal ou de faire une pause à l'écran.
Personnellement, je retourne 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é à zéro et d'utiliser une fonction LastError () pour fournir des détails sur l'erreur réelle.
Au lieu de renvoyer votre erreur, ce que vous pouvez faire et vous empêcher ainsi de renvoyer des données avec votre fonction, utilisez 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 très rentable. et vous n'aurez pas à pousser un autre argument sur la pile lorsque vous appelez votre fonction (returnedSize
ou returnedError
dans les deux méthodes que vous avez présentées).
EDIT: Si vous n'avez besoin que de l'accès à la dernière erreur et que vous ne travaillez pas dans un environnement multithread.
Vous pouvez uniquement renvoyer true/false (ou une sorte de #define si vous travaillez en C et ne supportez pas les variables bool), et disposez d'un tampon d'erreur global contenant 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
}
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
La deuxième approche permet au compilateur de produire un code plus optimisé, car lorsque l'adresse d'une variable est transmise à une fonction, le compilateur ne peut pas conserver sa valeur dans les registres lors d'appels ultérieurs à d'autres fonctions. Le code d'achèvement est généralement utilisé une seule fois, juste après l'appel, alors que les "vraies" données renvoyées par l'appel peuvent être utilisées plus souvent.
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 sur chaque appel, c'est-à-dire:
if( !doit(a, b, c, &errcode) )
{ (* handle *)
(* thine *)
(* error *)
}
Lorsque vous avez beaucoup de vérifications d'erreur, cette petite simplification est vraiment utile.