Les différentes façons de gérer les erreurs en C,
Le C ne dispose pas d'une seule façon claire pour gérer les erreurs
Le 2022-07-29 09:47:06, par Bruno, Chroniqueur Actualités
Le langage de programmation C offre au développeur une marge de contrôle importante sur la machine (notamment sur la gestion de la mémoire) et est de ce fait utilisé pour réaliser les « fondations » (compilateurs, interpréteurs…) des langages plus modernes. C'est un langage de programmation impératif généraliste, de bas niveau. Inventé au début des années 1970 pour réécrire Unix, C est devenu un des langages les plus utilisés, encore de nos jours. De nombreux langages plus modernes comme C++, C#, Java et PHP ou JavaScript ont repris une syntaxe similaire au C et reprennent en partie sa logique.
En tant que tel, le langage C ne fournit pas de support direct pour la gestion des erreurs, mais étant un langage de programmation système, il vous fournit un accès à un niveau inférieur sous la forme de valeurs de retour. La plupart des appels de fonctions C ou même Unix renvoient -1 ou NULL en cas d'erreur et définissent un code d'erreur errno. Ainsi, un programmeur C peut vérifier les valeurs retournées et peut prendre les mesures appropriées en fonction de la valeur de retour.
Essayons de simuler une condition d'erreur et d'ouvrir un fichier qui n'existe pas. Ici, les deux fonctions sont utilisées pour montrer leur usage, mais il est possible d'utiliser une ou plusieurs façons d'imprimer les erreurs. Le deuxième point important à noter est qu'il est possible d'utiliser le flux de fichiers stderr pour afficher toutes les erreurs.
Lorsque le code ci-dessus est compilé et exécuté, il produit le résultat suivant
Value of errno: 2
Error printed by perror: No such file or directory
Error opening file: No such file or directory
Le C n'a pas une seule façon claire de gérer les erreurs
Statut de sortie
Il fournit une fonction exit() qui prend deux valeurs pour afficher une fin réussie ou non réussie en utilisant EXIT_SUCCESS et EXIT_FAILURE. Cette fonction exit() est définie dans le fichier d'en-tête stdlib.h de la bibliothèque standard.
L'algorithme de l'autruche
Si une condition d'erreur est suffisamment rare, il est toujours possible de faire l'autruche et choisir d'ignorer cette possibilité. Cela peut rendre le code beaucoup plus joli, mais au détriment de la robustesse.
Expecting garbage or crash on bad values
parsed: 10
parsed: 6093
parsed: 42
parsed: 0
Un exemple concret de cela peut être observé avec l'utilisation de malloc par le micrologiciel des dispositifs de flipper.
Crash
Parfois, les erreurs sont pratiquement irrécupérables. Selon McCue, la plupart des applications devraient probablement abandonner lorsque malloc renvoie NULL. Si vous êtes sûr qu'il n'y a pas de moyen de récupérer une condition d'erreur et que l'appelant ne voudra pas la gérer d'une autre manière, il est simplement possible d'imprimer un message disant ce qui s'est mal passé et quitter le programme.
parsed: 10
parsed: 42
Got a bad character ('f') in foo, crashing.
Retourner un nombre négatif
Si la fonction renvoie normalement un nombre naturel, il est possible d'utiliser un nombre négatif pour indiquer un échec. Cela s'applique aussi bien à l'exemple qu'à des cas tels que le renvoi du nombre d'octets lus dans un fichier. S'il existe différents types d'erreurs pour ce genre de cas,il est également possible d'utiliser des nombres négatifs spécifiques pour indiquer les différentes catégories.
worked: 10
failed: foo
worked: 42
Retourner NULL
Si la fonction renvoie normalement un pointeur, il est possible d'utiliser NULL pour indiquer que quelque chose s'est mal passé. La plupart des fonctions qui renverraient des pointeurs effectueraient une allocation au tas pour que cela soit sain, donc ce schéma n'est probablement pas applicable lorsque vous voulez éviter les allocations. Pour McCue, il serait stupide d'allouer un [/C]int[/C] au tas.
worked: 10
failed: foo
worked: 42
Un exemple concret de ce schéma est celui de malloc. Si malloc ne parvient pas à allouer de la mémoire, au lieu de renvoyer un pointeur vers la mémoire nouvellement allouée, il renvoie un pointeur nul.
Retourner un booléen et prendre un paramètre externe
L'une des choses les moins évidentes qu'il est possible de faire en C est d'avoir un ou plusieurs arguments d'une fonction out params. Cela signifie qu'il fait partie du contrat de la fonction qu'elle écrira dans la mémoire derrière un pointeur. Si une fonction peut échouer, une traduction naturelle de ceci peut être de retourner un booléen indiquant si elle l'a fait et de passer un paramètre out qui n'est n'inspecté que lorsque true est retourné.
Retourner une énumération et prendre un paramètre de sortie
Un booléen peut seulement indiquer que quelque chose a réussi ou échoué. Si vous voulez savoir pourquoi quelque chose a échoué, remplacer un enum par un booléen est un mécanisme assez naturel.
worked: 10
failed because bad char: foo
worked: 42
failed because empty string
Retourner un booléen et prendre deux paramètres en sortie
Alors qu'un enum peut donner la "catégorie" d'une erreur, il n'a pas de place pour enregistrer des informations plus spécifiques que cela. Par exemple, il est assez raisonnable de vouloir savoir, si vous rencontrez un caractère inattendu, où se trouve ce caractère dans la chaîne de caractères. En ajoutant un deuxième paramètre out, il est possible d'avoir un endroit pour mettre cette information.
worked: 10
failed: foo
worked: 42
failed: 12a34
Retourner un enum et plusieurs paramètres de sortie
Une extension naturelle des deux modèles précédents est que si vous avez plusieurs façons dont un calcul peut échouer, il est possible de retourner un enumavec chaque façon et prendre un paramètre out pour chaque façon qui nécessiterait des données.
worked: 10
failed because bad char at index 0: foo
worked: 42
failed because empty string
number was too big. had 5 digits left: 99999999999999
Définir une valeur statique locale de thread
Une autre option consiste à définir, en cas d'erreur, une variable statique locale. Cela évite d'avoir à propager explicitement une erreur tout le long de la pile à partir de l'endroit où elle se produit et rend l'API "normale" de la fonction aussi propre et nette que les approches autruche ou crash. Une fois que vous avez défini la valeur statique locale du thread, vous pouvez soit :
parsed: 10
parsed: 42
error: foo
Un grand nombre d'apis intégrées utilisent une constante statique partagée appelée errno et si elles échouent, elles lui attribuent une valeur non nulle. Il existe ensuite des fonctions comme perror qui peuvent extraire des messages à partir du code d'erreur spécifique. Techniquement, il est possible utiliser errno aussi, tant que vos conditions d'erreur peuvent tenir dans son encodage int.
Des langages tels C2 ou C3 pour remplacer le C ?
Comme dit précédemment, la gestion des erreurs dans le langage de programmation C n'est pas prise en charge, car il fournit quelques fonctions et des valeurs de numéros d'erreur qui sont imprimées comme des messages d'erreur. Le lamgage ne dispose que d'un support de bibliothèque très limité : il faut ajouter des chemins de recherche pour les fichiers d'en-tête, inclure certains fichiers d'en-tête et établir des liens avec des bibliothèques statiques ou dynamiques. Ces étapes sont toutes séparées. Si vous appelez des fonctions de bibliothèque sans les lier, vous pouvez avoir des références non définies.
C2 a corrigé ce problème en faisant de l'utilisation de la bibliothèque une chose totalement automatique. Vous utilisez la bibliothèque ou vous ne l'utilisez pas. De plus, C2 supporte les bibliothèques sources. Il s'agit de bibliothèques qui sont utilisées sous forme de source (=C2). Cela permet une meilleure intégration et optimisation, en particulier lors de l'utilisation de nombreuses fonctions "simples" qui ne font que renvoyer un membre d'une structure opaque, par exemple. Cela permet également aux développeurs d'organiser leurs archives de code d'une manière beaucoup plus facile.
C3 est un langage de programmation système basé sur le C. C'est une évolution du C permettant les mêmes paradigmes et conservant la même syntaxe dans la mesure du possible. C3 a commencé comme une extension du langage C2 par Bas van den Berg. Il a évolué de manière significative, non seulement au niveau de la syntaxe mais aussi en ce qui concerne la gestion des erreurs, les macros, les génériques et les chaînes de caractères.
Source : Mccue
Et vous ?
Que pensez-vous du langage C ? Dépassé ou d'actaulité
Quel langage de programmation utilisez-vous pour contourner les manquements du C ?
À votre avis, C2 et C3 pourraient remplacer le C ?
N'aurait-il pas été plus bénéfique de conjuguer les efforts pour améliorer le C plutôt que de créer C2 ou encore C3 et peut être C4 ?
Voir aussi :
C3 : un langage de programmation système basé sur le C, permet un accès sécurisé aux tableaux, les conteneurs de haut niveau et manipulation des chaînes de caractères
Microsoft célèbre les 20 ans de .NET, son Framework de développement, les dépôts .NET seraient dans le top 30 des projets open source à plus haute vélocité sur GitHub depuis 2017
Microsoft a publié la version stable de Visual Studio 2022 avec une nouvelle expérience de rechargement à chaud pour les applications natives C++, cette version est disponible uniquement en 64 bits
Un développeur publie un langage de programmation qui peut être traduit automatiquement en C, C++, C#, Java, JavaScript, etc., avec une traduction rapide et sans machine virtuelle
En tant que tel, le langage C ne fournit pas de support direct pour la gestion des erreurs, mais étant un langage de programmation système, il vous fournit un accès à un niveau inférieur sous la forme de valeurs de retour. La plupart des appels de fonctions C ou même Unix renvoient -1 ou NULL en cas d'erreur et définissent un code d'erreur errno. Ainsi, un programmeur C peut vérifier les valeurs retournées et peut prendre les mesures appropriées en fonction de la valeur de retour.
Essayons de simuler une condition d'erreur et d'ouvrir un fichier qui n'existe pas. Ici, les deux fonctions sont utilisées pour montrer leur usage, mais il est possible d'utiliser une ou plusieurs façons d'imprimer les erreurs. Le deuxième point important à noter est qu'il est possible d'utiliser le flux de fichiers stderr pour afficher toutes les erreurs.
Code : |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #include <stdio.h> #include <errno.h> #include <string.h> extern int errno ; int main () { FILE * pf; int errnum; pf = fopen ("unexist.txt", "rb"); if (pf == NULL) { errnum = errno; fprintf(stderr, "Value of errno: %d\n", errno); perror("Error printed by perror"); fprintf(stderr, "Error opening file: %s\n", strerror( errnum )); } else { fclose (pf); } return 0; } |
Lorsque le code ci-dessus est compilé et exécuté, il produit le résultat suivant
Value of errno: 2
Error printed by perror: No such file or directory
Error opening file: No such file or directory
Le C n'a pas une seule façon claire de gérer les erreurs
Statut de sortie
Il fournit une fonction exit() qui prend deux valeurs pour afficher une fin réussie ou non réussie en utilisant EXIT_SUCCESS et EXIT_FAILURE. Cette fonction exit() est définie dans le fichier d'en-tête stdlib.h de la bibliothèque standard.
Code : |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #include <stdio.h> #include <errno.h> #include <string.h> #include <stdlib.h> int main () { FILE * f; f = fopen ("article.txt", "rb"); if (f == NULL) { printf("The Value of errno printed is : %d\n", errno); printf("Error message printed while opening the file with errno: %s\n", strerror(errno)); perror("Error message printed by perror"); exit(EXIT_FAILURE); printf("The message will not be printed\n"); } else { fclose (f); exit(EXIT_SUCCESS); printf("The message will be printed\n"); } return 0; } |
L'algorithme de l'autruche
Si une condition d'erreur est suffisamment rare, il est toujours possible de faire l'autruche et choisir d'ignorer cette possibilité. Cela peut rendre le code beaucoup plus joli, mais au détriment de la robustesse.
Code : |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #include <stdio.h> int parse_natural_base_10_number(const char* s) { int parsed = 0; for (size_t i = 0; s[i] != '\0'; i++) { parsed *= 10; parsed += s[i] - '0'; } return parsed; } int main() { printf("Expecting garbage or crash on bad values\n"); const char* examples[] = { "10", "foo", "42", "" }; for (size_t i = 0; i < 4; i++) { const char* example = examples[i]; int parsed = parse_natural_base_10_number(example); printf("parsed: %d\n", parsed); } return 0; } |
Expecting garbage or crash on bad values
parsed: 10
parsed: 6093
parsed: 42
parsed: 0
Un exemple concret de cela peut être observé avec l'utilisation de malloc par le micrologiciel des dispositifs de flipper.
Crash
Parfois, les erreurs sont pratiquement irrécupérables. Selon McCue, la plupart des applications devraient probablement abandonner lorsque malloc renvoie NULL. Si vous êtes sûr qu'il n'y a pas de moyen de récupérer une condition d'erreur et que l'appelant ne voudra pas la gérer d'une autre manière, il est simplement possible d'imprimer un message disant ce qui s'est mal passé et quitter le programme.
Code : |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | #include <stdio.h> #include <stdlib.h> int parse_natural_base_10_number(const char* s) { int parsed = 0; for (size_t i = 0; s[i] != '\0'; i++) { if (s[i] < '0' || s[i] > '9') { printf( "Got a bad character ('%c') in %s, crashing.", s[i], s ); exit(1); } else { parsed *= 10; parsed += s[i] - '0'; } } return parsed; } int main() { const char* examples[] = { "10", "42", "foo" }; for (size_t i = 0; i < 3; i++) { const char* example = examples[i]; int parsed = parse_natural_base_10_number(example); printf("parsed: %d\n", parsed); } return 0; } |
parsed: 10
parsed: 42
Got a bad character ('f') in foo, crashing.
Retourner un nombre négatif
Si la fonction renvoie normalement un nombre naturel, il est possible d'utiliser un nombre négatif pour indiquer un échec. Cela s'applique aussi bien à l'exemple qu'à des cas tels que le renvoi du nombre d'octets lus dans un fichier. S'il existe différents types d'erreurs pour ce genre de cas,il est également possible d'utiliser des nombres négatifs spécifiques pour indiquer les différentes catégories.
Code : |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | #include <stdio.h> int parse_natural_base_10_number(const char* s) { int parsed = 0; for (size_t i = 0; s[i] != '\0'; i++) { if (s[i] < '0' || s[i] > '9') { return -1; } else { parsed *= 10; parsed += s[i] - '0'; } } return parsed; } int main() { const char* examples[] = { "10", "foo", "42" }; for (size_t i = 0; i < 3; i++) { const char* example = examples[i]; int parsed = parse_natural_base_10_number(example); if (parsed < 0) { printf("failed: %s\n", example); } else { printf("worked: %d\n", parsed); } } return 0; } |
worked: 10
failed: foo
worked: 42
Retourner NULL
Si la fonction renvoie normalement un pointeur, il est possible d'utiliser NULL pour indiquer que quelque chose s'est mal passé. La plupart des fonctions qui renverraient des pointeurs effectueraient une allocation au tas pour que cela soit sain, donc ce schéma n'est probablement pas applicable lorsque vous voulez éviter les allocations. Pour McCue, il serait stupide d'allouer un [/C]int[/C] au tas.
Code : |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | #include <stdio.h> #include <stdlib.h> int* parse_natural_base_10_number(const char* s) { int parsed = 0; for (size_t i = 0; s[i] != '\0'; i++) { if (s[i] < '0' || s[i] > '9') { return NULL; } else { parsed *= 10; parsed += s[i] - '0'; } } int* result = malloc(sizeof (int)); *result = parsed; return result; } int main() { const char* examples[] = { "10", "foo", "42" }; for (size_t i = 0; i < 3; i++) { const char* example = examples[i]; int* parsed = parse_natural_base_10_number(example); if (parsed == NULL) { printf("failed: %s\n", example); } else { printf("worked: %d\n", *parsed); } free(parsed); } return 0; } |
worked: 10
failed: foo
worked: 42
Un exemple concret de ce schéma est celui de malloc. Si malloc ne parvient pas à allouer de la mémoire, au lieu de renvoyer un pointeur vers la mémoire nouvellement allouée, il renvoie un pointeur nul.
Retourner un booléen et prendre un paramètre externe
L'une des choses les moins évidentes qu'il est possible de faire en C est d'avoir un ou plusieurs arguments d'une fonction out params. Cela signifie qu'il fait partie du contrat de la fonction qu'elle écrira dans la mémoire derrière un pointeur. Si une fonction peut échouer, une traduction naturelle de ceci peut être de retourner un booléen indiquant si elle l'a fait et de passer un paramètre out qui n'est n'inspecté que lorsque true est retourné.
Code : |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | #include <stdio.h> #include <stdbool.h> bool parse_natural_base_10_number(const char* s, int* out) { int parsed = 0; for (size_t i = 0; s[i] != '\0'; i++) { if (s[i] < '0' || s[i] > '9') { return false; } else { parsed *= 10; parsed += s[i] - '0'; } } *out = parsed; return true; } int main() { const char* examples[] = { "10", "foo", "42" }; for (size_t i = 0; i < 3; i++) { const char* example = examples[i]; int parsed; bool success = parse_natural_base_10_number( example, &parsed ); if (!success) { printf("failed: %s\n", example); } else { printf("worked: %d\n", parsed); } } return 0; } |
Retourner une énumération et prendre un paramètre de sortie
Un booléen peut seulement indiquer que quelque chose a réussi ou échoué. Si vous voulez savoir pourquoi quelque chose a échoué, remplacer un enum par un booléen est un mécanisme assez naturel.
Code : |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | #include <stdio.h> enum ParseNaturalNumberResult { PARSE_NATURAL_SUCCESS, PARSE_NATURAL_EMPTY_STRING, PARSE_NATURAL_BAD_CHARACTER } ; enum ParseNaturalNumberResult parse_natural_base_10_number( const char* s, int* out ) { if (s[0] == '\0') { return PARSE_NATURAL_EMPTY_STRING ; } int parsed = 0 ; for (size_t i = 0 ; s[i] != '\0' ; i++) { if (s[i] < '0' || s[i] > '9') { return PARSE_NATURAL_BAD_CHARACTER ; } else { parsed *= 10 ; parsed += s[i] - '0' ; } } *out = parsed ; return PARSE_NATURAL_SUCCESS ; } int main() { const char* examples[] = { "10", "foo", "42", "" } ; for (size_t i = 0 ; i < 4 ; i++) { const char* exemple = exemples[i] ; int parsed ; switch (parse_natural_base_10_number(example, &parsed)) { cas PARSE_NATURAL_SUCCESS : printf("a fonctionné : %d\n", parsed) ; pause ; cas PARSE_NATURAL_EMPTY_STRING : printf("failed because empty string\n") ; pause ; cas PARSE_NATURAL_BAD_CHARACTER : printf("failed because bad char : %s\n", exemple) ; break ; } } return 0 ; } |
failed because bad char: foo
worked: 42
failed because empty string
Retourner un booléen et prendre deux paramètres en sortie
Alors qu'un enum peut donner la "catégorie" d'une erreur, il n'a pas de place pour enregistrer des informations plus spécifiques que cela. Par exemple, il est assez raisonnable de vouloir savoir, si vous rencontrez un caractère inattendu, où se trouve ce caractère dans la chaîne de caractères. En ajoutant un deuxième paramètre out, il est possible d'avoir un endroit pour mettre cette information.
Code : |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | #include <stdio.h> #include <stdbool.h> bool parse_natural_base_10_number( const char* s, int* out_value, size_t* out_bad_index ) { int parsed = 0; for (size_t i = 0; s[i] != '\0'; i++) { if (s[i] < '0' || s[i] > '9') { *out_bad_index = i; return false; } else { parsed *= 10; parsed += s[i] - '0'; } } *out_value = parsed; return true; } int main() { const char* examples[] = { "10", "foo", "42", "12a34" }; for (size_t i = 0; i < 4; i++) { const char* example = examples[i]; int parsed; size_t bad_index; bool success = parse_natural_base_10_number( example, &parsed, &bad_index ); if (!success) { printf("failed: %s\n ", example); for (size_t j = 0; j < bad_index; j++) { printf(" "); } printf("\n"); } else { printf("worked: %d\n", parsed); } } return 0; } |
worked: 10
failed: foo
worked: 42
failed: 12a34
Retourner un enum et plusieurs paramètres de sortie
Une extension naturelle des deux modèles précédents est que si vous avez plusieurs façons dont un calcul peut échouer, il est possible de retourner un enumavec chaque façon et prendre un paramètre out pour chaque façon qui nécessiterait des données.
Code : |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | #include <stdio.h> #include <string.h> enum ParseNaturalNumberResult { PARSE_NATURAL_SUCCESS, PARSE_NATURAL_EMPTY_STRING, PARSE_NATURAL_BAD_CHARACTER, PARSE_NUMBER_TOO_BIG }; struct BadCharacterInfo { size_t index; }; struct TooBigInfo { size_t remaining_characters; }; enum ParseNaturalNumberResult parse_natural_base_10_number( const char* s, int* out_value, struct BadCharacterInfo* bad_character_info, struct TooBigInfo* too_big_info ) { if (s[0] == '\0') { return PARSE_NATURAL_EMPTY_STRING; } int parsed = 0; for (size_t i = 0; s[i] != '\0'; i++) { if (s[i] < '0' || s[i] > '9') { bad_character_info->index = i; return PARSE_NATURAL_BAD_CHARACTER; } else { int digit = s[i] - '0'; int new_parsed = (parsed * 10) + digit; if ((new_parsed - digit) / 10 != parsed) { too_big_info->remaining_characters = strlen(s) - i; return PARSE_NUMBER_TOO_BIG; } else { parsed = new_parsed; } } } *out_value = parsed; return PARSE_NATURAL_SUCCESS; } int main() { const char* examples[] = { "10", "foo", "42", "", "99999999999999" }; for (size_t i = 0; i < 5; i++) { const char* example = examples[i]; int parsed; struct BadCharacterInfo bad_character_info; struct TooBigInfo too_big_info; switch (parse_natural_base_10_number( example, &parsed, &bad_character_info, &too_big_info )) { case PARSE_NATURAL_SUCCESS: printf("worked: %d\n", parsed); break; case PARSE_NATURAL_EMPTY_STRING: printf("failed because empty string\n"); break; case PARSE_NATURAL_BAD_CHARACTER: printf( "failed because bad char at index %zu: %s\n", bad_character_info.index, example ); break; case PARSE_NUMBER_TOO_BIG: printf( "number was too big. had %zu digits left: %s\n", too_big_info.remaining_characters, example ); break; } } return 0; } |
worked: 10
failed because bad char at index 0: foo
worked: 42
failed because empty string
number was too big. had 5 digits left: 99999999999999
Définir une valeur statique locale de thread
Une autre option consiste à définir, en cas d'erreur, une variable statique locale. Cela évite d'avoir à propager explicitement une erreur tout le long de la pile à partir de l'endroit où elle se produit et rend l'API "normale" de la fonction aussi propre et nette que les approches autruche ou crash. Une fois que vous avez défini la valeur statique locale du thread, vous pouvez soit :
- Retourner une valeur prévisible indiquant un problème (NULL, un nombre négatif, etc.), ce qui incite le programmeur à vérifier la valeur statique locale du thread.
- Retourner une valeur non initialisée et compter sur le programmeur pour savoir que la valeur pourrait être fausse à moins qu'il ne vérifie la valeur statique locale du thread.
Code : |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | #include <stdio.h> #include <stdbool.h> _Thread_local static bool parse_number_error = false; int parse_natural_base_10_number(const char* s) { int parsed = 0; for (size_t i = 0; s[i] != '\0'; i++) { if (s[i] < '0' || s[i] > '9') { parse_number_error = true; } else { parsed *= 10; parsed += s[i] - '0'; } } return parsed; } int main() { const char* examples[] = { "10", "42", "foo" }; for (size_t i = 0; i < 3; i++) { const char* example = examples[i]; int parsed = parse_natural_base_10_number(example); if (parse_number_error) { parse_number_error = false; printf("error: %s\n", example); } else { printf("parsed: %d\n", parsed); } } return 0; } |
parsed: 10
parsed: 42
error: foo
Un grand nombre d'apis intégrées utilisent une constante statique partagée appelée errno et si elles échouent, elles lui attribuent une valeur non nulle. Il existe ensuite des fonctions comme perror qui peuvent extraire des messages à partir du code d'erreur spécifique. Techniquement, il est possible utiliser errno aussi, tant que vos conditions d'erreur peuvent tenir dans son encodage int.
Des langages tels C2 ou C3 pour remplacer le C ?
Comme dit précédemment, la gestion des erreurs dans le langage de programmation C n'est pas prise en charge, car il fournit quelques fonctions et des valeurs de numéros d'erreur qui sont imprimées comme des messages d'erreur. Le lamgage ne dispose que d'un support de bibliothèque très limité : il faut ajouter des chemins de recherche pour les fichiers d'en-tête, inclure certains fichiers d'en-tête et établir des liens avec des bibliothèques statiques ou dynamiques. Ces étapes sont toutes séparées. Si vous appelez des fonctions de bibliothèque sans les lier, vous pouvez avoir des références non définies.
C2 a corrigé ce problème en faisant de l'utilisation de la bibliothèque une chose totalement automatique. Vous utilisez la bibliothèque ou vous ne l'utilisez pas. De plus, C2 supporte les bibliothèques sources. Il s'agit de bibliothèques qui sont utilisées sous forme de source (=C2). Cela permet une meilleure intégration et optimisation, en particulier lors de l'utilisation de nombreuses fonctions "simples" qui ne font que renvoyer un membre d'une structure opaque, par exemple. Cela permet également aux développeurs d'organiser leurs archives de code d'une manière beaucoup plus facile.
C3 est un langage de programmation système basé sur le C. C'est une évolution du C permettant les mêmes paradigmes et conservant la même syntaxe dans la mesure du possible. C3 a commencé comme une extension du langage C2 par Bas van den Berg. Il a évolué de manière significative, non seulement au niveau de la syntaxe mais aussi en ce qui concerne la gestion des erreurs, les macros, les génériques et les chaînes de caractères.
Source : Mccue
Et vous ?
Voir aussi :
-
WhiteCrowMembre expérimentéBonjour,
une «autre méthode» (classique) consiste à utiliser errno pour indiquer le statut du résultat. On assigne 0 à errno, on effectue l'opération, si une erreur survient alors errno n'est plus nul et contient un code erreur ; on pourra avantageusement réutiliser les codes standards. Par exemple :
Code : 1
2
3
4
5
6
7
8errno=0; int n=parse_natural_base_10_number(test_string); if (errno) { perror("parsing failed"); } else { printf("parsed %d\n", n); }
Sinon, dans le même genre, on peut créer un type adéquat. On ne parse pas une chaîne, on essaye de la parser et du coup le résultat d'une telle opération devrait être non un entier mais un type représentant soit un (succès, entier) soit un (échec, raison). Par exemple :
Code : 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23enum try_parse_error { INVALID_CHAR, RANGE }; const char *try_parse_error_str[] = { [INVALID_CHAR]="unexpected char", [RANGE]="integer range error", }; struct try_parse_int_result { bool success; union { int value; enum try_parse_error error; } } ... struct try_parse_int_result result=try_parse_int(test_string); if (result.success) { printf("parsed %d\n", result.value); } else { printf("parsing failed with error : %s\n", try_parse_error_str[result.error]); }
le 29/07/2022 à 10:23 -
imperioMembre émériteJusqu'au jour où tu fais du multi-threading. Les variables globales comme errno deviennent beaucoup moins fun d'un coup.le 29/07/2022 à 13:12
-
WhiteCrowMembre expérimentéDe nos jours errno est thread local … donc moins de soucis de côté là. En revanche il y a toujours un problème de réentrance dans un même thread, genre un signal handler qui se déclenche et modifie errno.
Il faut clairement plus de rigueur quand on utilise errno que lors qu'on définit un type adapté, chose que l'on retrouve dans la plupart des langages plus récents.le 29/07/2022 à 14:56 -
SofEvansMembre émériteEt pis tiens, pour rajouter ma petite pierre à l’édifice.
L'article parle plutôt bien des différentes méthodes pour gérer les erreurs, mais deux/trois petits trucs en plus :
Déjà, on ne gère pas les erreurs de la même manière si on est en train de coder une bibliothèque ou un programme.
La politique du "j'ai une erreur, je crash", c'est quelque chose qui me fait vraiment rager quand je trouve ça dans une bibliothèque.
Une bibliothèque ne devrait jamais crasher, mais toujours retourner la main à la fonction appelante (à charge de celui qui utilise la bibliothèque de gérer).
Bien sûr, celui qui fait la bibliothèque doit bien faire attention à gérer les erreurs de manière a ce que l'appelant puisse gérer de son côté (et la, l'article détail bien).
J'ai personnellement une préférence pour une variable globale local au thread à la manière d'errno (si c'est utile, bien sûr).
Idem pour la politique de l'autruche : il vaut mieux éviter ça dans une bibliothèque.
Rien de plus énervant que de déboguer pendant plusieurs jours pour se rendre compte qu'un cas extrêmement rare (souvent issue d'une race condition, histoire de rajouter du fun) n'as pas été testé (et provoque une erreur silencieuse, toujours pour plus de fun).
Le petit plus sympathique : quand la bibliothèque fait des logs et te permet de rediriger les logs.
Bon usuellement c'est stderr et ça fait très bien le taff, mais des fois pouvoir dire "le fichier de log, c'est celui-là", ça peut être sympa.
Pour finir, je prêche ma petite paroisse : n'ayez pas peur d'utiliser GOTO pour la gestion d'erreur.
Je remet un exemple :Code : 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46bool MyFunction(void) { char *logPathfile = NULL; FILE *logFile = NULL; char *msg = NULL; bool returnValue = false; logPathfile = malloc(...); if (!logPathfile) { // Message erreur (possible d'utiliser perror (3) / strerror (3)) goto END_FUNCTION; } sprintf(logPathfile, "%s", "/home/user/exemple.txt"); logFile = fopen(logPathfile, "w"); if (!logFile) { // Message erreur (possible d'utiliser perror (3) / strerror (3)) goto END_FUNCTION; } msg = malloc(...); if (!msg) { // Message erreur (possible d'utiliser perror (3) / strerror (3)) goto END_FUNCTION; } /* .. le reste du code, avec possiblement d autres tests */ // Fin de la fonction returnValue = true; /* GOTO */END_FUNCTION: free(logPathfile); if (logFile) { fclose(logFile); } free(msg); return returnValue; }
On peut aussi implémenter "simplement" des actions de rollback.
Imaginez que la fonction précédente ait pour but de créer un nouveau fichier et qu'en cas d'erreur, le fichier doit être supprimé afin de laisser le système propre.
On peut donc ajouter à un seul endroit :Code : 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23... // Fin de la fonction returnValue = true; /* GOTO */END_FUNCTION: /* action de rollback */ if (!returnValue) { if (logPathfile) { remove(logPathfile); } } /* action de clean memoire */ free(logPathfile); if (logFile) { fclose(logFile); } free(msg); return returnValue; }
Après tout est question de mesure : si vos actions de rollback prennent 200 lignes et font 48 actions, c'est p't'être qu'il faudrait organiser différemment le code.
Bref, goto c'est bien, goto c'est cool, n'hésitez pas à l'utiliser pour la gestion d'erreur en C. Pour les autres utilisations, c'est à vos risque et péril.le 02/08/2022 à 12:14 -
SofEvansMembre émériteUsuellement, la callstack est disponible en phase de développement via l'utilisation d'un débugueur (plus pratique que le printf).
En phase de prod, c'est normalement pas possible car les binaires/bibliothèques sont généralement compilé sans les symboles (symboles comprenant les noms de fonction entre autres).
Après, si tu veux avoir une callstack sur un log d'une erreur de prod, y'a pas 36 moyens et ils sont tous (à ma connaissance) dépendant du système et limité.
Sur linux par exemple, tu peux utiliser backtrace (3) (puis backtrace_symbols (3)) mais pour avoir quelques chose d'exploitable, il faut linker avec l'option "-rdynamic".
Et même avec tout ça, le nom des fonctions static n'apparaissent pas et le nom des fonctions des bibliothèque qui n'ont pas étés compilées avec -rdynamic non plus.le 02/08/2022 à 9:52 -
WhiteCrowMembre expérimentéNe «JAMAIS» être nécessaire ne signifie pas pour autant «ne pas être UTILES» : la preuve en Java … les break et continue, nommés ou non, ne sont que du sucre syntaxique pour des «goto contraints».
Il faut arrêter la simplification militante nécessaire au débutant du «goto = mal». Un débutant doit d'abord apprendre à programmer avant d'apprendre à coder, d'où la tonne de «ne fais pas ça ou sinon tu iras en enfer» ; et avec l'expérience, on s'aperçoit vite qu'en C ou dans d'autres langages un goto peut rendre un code plus lisible. Mais bon, on sait ça depuis les années 60 et l'émergence du paradigme de la programmation structurée, rien de neuf sous le soleil.le 05/08/2022 à 21:58 -
PyramidevExpert éminentgoto ne devient inutile qu'après avoir remplacé ses utilisations légitimes possibles par d'autres fonctionnalités plus ciblées.
Par exemple, la boucle while :
Code : 1
2
3
4while(condition) { foo(); } bar();
Code : 1
2
3
4
5
6
7begin_loop: if(!condition) goto end_loop; foo(); goto begin_loop; end_loop: bar();
Dans cet exemple, en Java, on n'aurait pas utilisé goto. La fermeture du fichier aurait été gérée par un try-with-resources (ou un try/finally dans les anciennes versions) et la libération de la mémoire aurait été directement gérée par le ramasse-miettes.
En C++ et en Rust, on aurait utilisé le RAII.
La plupart des langages ont des fonctionnalités dédiées à la libération des ressources qui remplacent les usages de goto, mais pas le langage C.le 05/08/2022 à 23:02 -
Sve@rExpert éminent séniorBonjour
Solution très éléganteMais je préfère celle du errno qui me semble être justement le truc fait pour ça. le 29/07/2022 à 12:26 -
Rep.MovsMembre habituéJe ne trouve pas le C "dépassé". Le C est utile dans des programmes bas niveau, proches CPU (quand on tape du C, on peut presque imaginer l'assembleur derrière). Faire des pilotes sans C (ou sans langage bas niveau), c'est difficile à imaginer.
Par contre, le C dans une appli de gestion, c'est un non direct. Il y a des langages plus adaptés.
De bons langages, il y en a eu plein - je ne suis pas sûr qu'il faille éternellement en ajouter (certains pas très bien réfléchis d'ailleurs...). Et utiliser des modificateurs à la compilation pour ajouter des comportements n'est pas non plus toujours souhaitable.
Ce qui prime au-delà du langage, c'est les bibliothèques disponibles et son interropérabilité.
Finalement, je choisirais facilement un langage plus haut niveau dès que j'ai une hétérogénéïté des formats de chaînes à traiter (UTF8,ASCII, autre...)le 02/08/2022 à 9:20 -
SofEvansMembre émérite* Se connecte à developpez
* Voit que le sujet sur la gestion d'erreur C est passé de 16 à 25 réponse
* Se dit "Nom de dieu, qu'est-ce qu'il s'est passé ? Y-a-t’il eu un débat endiablé très intéressant apportant plein d'information que je ne connais pas ??"goto est une instruction inutile. La preuve avec Java qui ne supporte pas le goto
Ah p*tain ...
Bon ben je vais me chercher une bière ... ça sera plus intéressant.
Sérieusement Jacti ?
Tu savais aussi que while c'est une instruction inutile ?
Bahoui, on peut remplacer tout les while par des for.
Et en plus, le mot clef while n'existe pas en Go, alors franchement si ça c'est pas de la preuve bétonné !
Pis tant qu'a faire, tu savais aussi que "++" c'est une instruction totalement inutile ?
Ébahoui, on peut remplacer tout les "i++" par des "i += 1".
D'ailleurs, Ruby n'as pas d'opérateur de pré/post incrémentation ! Abahçaalors, j'aurais dû faire avocat, j'aurais perdu aucune affaire.
Bon, histoire de ne pas faire que du Troll, je rajoute une petite brique ainsi que quelques exemple.
Déjà, je considère que les méthodes de gestion d'erreur présenté dans le premier post sont "commune" et "traditionnelle".
Je veux dire par la, c'est que dans 95% des cas, on tomberas sur une gestion d'erreur de ce type.
Ensuite, je suis plutôt partisans d'utiliser toujours la gestion la plus "simple" et la plus "logique".
Je ne suis pas pour faire toujours la même gestion : ça n'as aucun sens de faire une gestion à base de goto si on a aucune variable a nettoyer et aucune action de rollback à faire.
Si en plus la fonction n'est pas susceptible d'évoluer significativement, franchement, c'est ridicule de faire du goto.
Par exemple, quand je fait une structure, je code plusieurs fonctions permettant de gérer/manipuler cette dernière.
Si la structure possède des propriétés à nettoyer, j'ai une fonction "NomStruct_Constructor" et une fonction "NomStruct_Destructor".
Le principe est simple : la première fonction appelé pour toute variable de type dia_s c'est le constructeur, la dernière, c'est le destructeur.Code : 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19/* dia : dynamic int array */ typedef struct dia { int *array; size_t size; /* Array size */ size_t nbElement; /* Number of element in array */ } dia_s; void Dia_Constructor(dia_s *self) { self->array = NULL; self->size = 0; self->nbElement = 0; } void Dia_Destructor(dia_s *self) { free(self->array); }
Si ensuite je veux pouvoir faire une allocation dynamique d'une variable dia_s, je met à disposition les fonctions "NomStruct_New" et une fonction "NomStruct_Delete".Code : 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21dia_s *Dia_New(void) { dia_s *new = NULL; new = malloc(sizeof(*new)); if (!new) { // Message erreur (possible d'utiliser perror (3) / strerror (3)) } else { Dia_Constructor(&new); } return new; } void Dia_Delete(dia_s *self) { if (self) { Dia_Destructor(self); free(self); } }
La fonction n'est absolument pas censé évoluer, c'est complétement ridicule d'utiliser goto dans cette situation (1 test avec 1 variable) et l'utilisation de goto complexifierais le code pour rien.
Par contre, si je devais faire une fonction prenant un chemin vers un fichier et parsant le fichier pour peupler le tableau dynamique, c'est plus probable que j'utiliserais goto (erreur ouverture fichier, erreur lecture fichier, erreur de format, erreur de données [int], données en overflow [int] ...).
Tiens, petit truc que j'aime bien faire avec mes structures :Code : 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/* dia : dynamic int array */ typedef struct dia { int *array; size_t size; /* Array size */ size_t nbElement; /* Number of element in array */ } dia_s; #define DIA_CONSTRUCTOR { \ .array = NULL, \ .size = 0, \ .nbElement = 0 \ } void Dia_Constructor(dia_s *self) { dia_s clean = DIA_CONSTRUCTOR; *self = clean; }
Ca me permet de pouvoir déclarer et initialiser une variable en même tempsle 06/08/2022 à 18:49