IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

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 , par Bruno

20PARTAGES

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

Code : Sélectionner tout
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 : Sélectionner tout
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 : Sélectionner tout
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 : Sélectionner tout
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 : Sélectionner tout
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 : Sélectionner tout
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 : Sélectionner tout
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 : Sélectionner tout
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 ;
}
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.

Code : Sélectionner tout
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 : Sélectionner tout
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 :

  1. 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.
  2. 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 : Sélectionner tout
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.

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

Une erreur dans cette actualité ? Signalez-le nous !

Avatar de WhiteCrow
Membre éprouvé https://www.developpez.com
Le 29/07/2022 à 10:23
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 : Sélectionner tout
1
2
3
4
5
6
7
8
 
    errno=0;
    int n=parse_natural_base_10_number(test_string);
    if (errno) {
        perror("parsing failed");
    } else {
        printf("parsed %d\n", n);
    }
parse_natural_base_10_number pourrait par exemple utiliser EINVAL si la chaîne contient des caractères illégaux, ERANGE si la chaîne contient bien un nombre mais irreprésentable en int, etc.

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 : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 
    enum 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]);
    }
11  0 
Avatar de imperio
Membre chevronné https://www.developpez.com
Le 29/07/2022 à 13:12
Citation Envoyé par Sve@r Voir le message
Bonjour

Solution très élégante Mais je préfère celle du errno qui me semble être justement le truc fait pour ça.
Jusqu'au jour où tu fais du multi-threading. Les variables globales comme errno deviennent beaucoup moins fun d'un coup.
9  0 
Avatar de WhiteCrow
Membre éprouvé https://www.developpez.com
Le 29/07/2022 à 14:56
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.
8  0 
Avatar de SofEvans
Membre chevronné https://www.developpez.com
Le 02/08/2022 à 9:52
Citation Envoyé par boboss123 Voir le message
Et comment vous faite pour afficher les noms des fonctions qui ont appelées la fonction qui génère l'erreur ?... c'est très pratique pour le debug
Usuellement, 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.
5  0 
Avatar de SofEvans
Membre chevronné https://www.developpez.com
Le 02/08/2022 à 12:14
Et 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 : Sélectionner tout
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
bool 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;
}
Ainsi, de cette manière, vous pouvez être assuré de ne pas faire de fuite de mémoire quelque soit l'endroit où le code échoue, et l'ajout d'autres variables à nettoyer se fait simplement, même si la fonction est longue.

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 : Sélectionner tout
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.
5  0 
Avatar de Sve@r
Expert éminent sénior https://www.developpez.com
Le 29/07/2022 à 12:26
Bonjour
Citation Envoyé par WhiteCrow Voir le message
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).
Solution très élégante Mais je préfère celle du errno qui me semble être justement le truc fait pour ça.
4  0 
Avatar de Pyramidev
Expert confirmé https://www.developpez.com
Le 05/08/2022 à 23:02
Citation Envoyé par Jacti Voir le message
1) goto est une instruction inutile. La preuve avec Java qui ne supporte pas le goto (c'est un mot réservé qui ne fait aucune action). Il est réservé afin de ne pas pouvoir l'utiliser comme identificateur de variable ou nom de fonction. Je n'ai jamais programmé un goto dans quelque langage que ce soit durant mes 40 ans de carrière (je ne parle pas de l'assembleur évidemment).
goto 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 : Sélectionner tout
1
2
3
4
    while(condition) {
        foo();
    }
    bar();
permet de remplacer le code :
Code : Sélectionner tout
1
2
3
4
5
6
7
begin_loop:
    if(!condition)
        goto end_loop;
    foo();
    goto begin_loop;
end_loop:
    bar();
Mais les fonctionnalités offertes par le langage C ne suffisent pas pour qu'il soit sage de bannir entièrement goto. En effet, en langage C, pour gérer la libération des ressources, il est pertinent d'utiliser goto. SofEvans a donné un exemple avec une fermeture de fichier et des libérations de mémoire :
Citation Envoyé par SofEvans Voir le message
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 : Sélectionner tout
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
bool 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;
}
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.
4  0 
Avatar de Rep.Movs
Membre régulier https://www.developpez.com
Le 02/08/2022 à 9:20
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...)
3  0 
Avatar de Sve@r
Expert éminent sénior https://www.developpez.com
Le 02/08/2022 à 16:48
Citation Envoyé par SofEvans Voir le message
Il faut dire que la norme limitait le nombre de ligne qu'une fonction (24 de mémoire) peut avoir ainsi que le nombre de colonne par ligne (80 caractère max).
Ah oui, ces "pseudo-règles" de normalisation et d'efficacité édictées par des profs qui n'ont jamais fait de C.
Une fonction doit faire un job et un seul (elle peut alors être réutilisées dans différents contextes, principe du légo). Elle peut prendre pour ça 10 ou 100 lignes, tant que ce qui se fait est bien expliqué (et autrement que par des i++; // J'incrémente i) alors pas de souci.

Citation Envoyé par SofEvans Voir le message
ça devenait ça : for (var_int_1 = 10, var_int_2 = 20, var_str_1 = "toto", i = 0; str[i]; ++i). Hop, 3 lignes de gagné ...
Effectivement le résultat logique et évidemment totalement contre-productif de cette inquisition. Normalement, face à un tel résultat, n'importe quel prof de correctement câblé remettrait en cause cette doctrine. Mais quand l'idéologie d'un idéologue ne marche pas, il en déduit toujours qu'il n'y a pas assez d'idéologie. Total, on interdit le for().

Résultat: while (var_int_1 = 10, var_int_2 = 20, var_str_1 = "toto", i = 0, str[i++]). On va faire quoi ensuite? Interdire le while ???

Citation Envoyé par SofEvans Voir le message
Et j'imagine qu'aujourd'hui encore, leurs normes alacon existe toujours et n'as pas changé ...
Tu as ta réponse.

Citation Envoyé par SofEvans Voir le message
Il suffit de bien expliquer, un débutant n'est pas un idiot, il peut comprendre.
Il n'est pas idiot mais il n'a pas encore l'expérience et le recul. Déjà on lui dit qu'il faut checker le retour de malloc() et de fopen() il va râler (lui il veut voir son code fonctionner au plus vite et pas coder des alternatives qu'il pense inutiles). Puis il ne "sent" pas comment faire si malloc() ou fopen() échouent (il n'a pas encore la "vision" des appels). Donc voilà, il est assez désarmé pour bien comprendre les avantages de ta méthode (j'ai beaucoup aimé tes exemples et si tu fouilles un peu mes posts tu devrais trouver les mêmes).

Citation Envoyé par SofEvans Voir le message
C'est un peu comme les variables globales : on les diabolise, on dit qu'ils faut jamais les utiliser mais quand tu dois gérer les signaux tu te retrouves à être obligé de les utiliser.
Un peu comme avec n'importe quelle fonction de callback qui n'accepte pas de paramètre (coucou qsort ....).
Là je nuancerai un peu. Les globales c'est comme les goto: à n'utiliser que si nécessaire Bref on a des outils, certains sont dangereux (scie sauteuse) mais parfois utiles (scier un gros truc).
3  0 
Avatar de WhiteCrow
Membre éprouvé https://www.developpez.com
Le 05/08/2022 à 21:58
Citation Envoyé par Jacti Voir le message
Les goto ne sont JAMAIS nécessaires : la preuve en Java

[...]
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.
3  0