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

89PARTAGES

8  3 

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

Avatar de WhiteCrow
Membre expérimenté 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 émérite 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.
11  0 
Avatar de WhiteCrow
Membre expérimenté 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 émérite 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.
6  0 
Avatar de SofEvans
Membre émérite 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 WhiteCrow
Membre expérimenté 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.
5  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.
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 Rep.Movs
Membre habitué 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...)
4  0 
Avatar de SofEvans
Membre émérite https://www.developpez.com
Le 06/08/2022 à 18:49
* 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 : Sélectionner tout
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 : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
dia_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);
    }
}
Le point que je veux mettre en avant, c'est que je n'ai pas utiliser goto dans Dia_New.
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 : Sélectionner tout
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 temps
4  0