Topic "Pour l'amour du C, n'utilisez pas les fonctions atoi, atol, atof et dérivées"

Flux RSS des posts récents dans ce topic ( Flux Atom)


Photo de profil de skywodd

skywodd

Membre

Membre du staff

#107 | Signaler ce message


Dans cet article, nous allons discuter d'une erreur très fréquente en développement informatique et tout particulièrement en programmation C/C++ (et par extension en programmation Arduino) : l'utilisation des fonctions atoi(), atol(), atof() et dérivées. Nous verrons ensemble pourquoi ces fonctions posent problème et comment les remplacer par du code plus robuste.

Lire la suite de l'article sur le site


Pas de photo de profil

Sanpi

Membre

#108 | Signaler ce message


Ne pas utiliser les fonctions ato* est une bonne chose, mais je ne suis pas certain que sscanf soit une bonne alternative.

Il est bien plus simple d’utiliser strtol ou strtod.


Photo de profil de skywodd

skywodd

Membre

Membre du staff

#109 | Signaler ce message


Il est bien plus simple d’utiliser strtol ou strtod.

par Sanpi

On m'a fait la même remarque sur Twitter. Je vais donc profiter du forum pour expliquer mon point de vue en plus de 140 caractères ;)

Pour moi, les fonctions strtoX() sont très pratiques quand on sait ce que l'on fait et qu'on lit la doc. C'est aussi le cas des fonctions atoX(), si il y a vérification de saisie en amont.

MAIS, cet article existe car justement, plus personne ne prend le temps de lire la doc avant de copier coller du code.

Avec strtod par exemple, pour détecter une erreur il y a deux solutions :

  • tester la valeur du pointeur (optionnel) de fin de chaîne de caractères,

  • tester la valeur de errno après l'appel à la fonction.

Dans le cas du pointeur de fin de chaîne de caractères, il faut déjà prendre le temps de l'utiliser, ce n'est pas un paramètre obligatoire. Il faut ensuite prendre bien soin de passer un pointeur sur le pointeur et pas le pointeur lui même. Il faut ensuite faire le test de la valeur du pointeur après l'appel.

Pour résumer, il faut faire ça :

1
2
3
4
5
6
7
char* str = "Foobar";
char* endPtr;

double valeur = strtod(str, &endPtr);
if (endPtr == str) {
   // Erreur ...
}

Pour un débutant ou un développeur en manque de café, ça va automatiquement se finir par un "la flemme" ou "un pointeur sur pointeur !? HEIN !?".

N.B. La façon dont la documentation explique la valeur du pointeur en cas d'erreur est si bien écrite que je suis incapable de vous dire si mon test si dessus gère des cas d'erreur comme "42azerty".

Dans le cas de errno, il faut d'abord mettre errno à zéro, faire l'appel, puis vérifier errno.

Pour résumer, il faut faire ça :

1
2
3
4
5
6
7
char* str = "Foobar";

errno = 0;
double valeur = strtod(str, NULL);
if (errno == ERANGE) {
   // Erreur ...
}

Sauf que, pas de bol, suivant l'implémentation, errno n'est pas forcément utilisé. Sur Twitter par exemple, on me parlait de Nuttx (un RTOS pour microcontrôleurs ARM). Et bien dans Nuttx, errno n'est pas utilisé.

Par conséquent, en suivant les bonnes pratiques, voila ce qu'il faudrait normalement faire :

1
2
3
4
5
6
7
8
char* str = "Foobar";
char* endPtr;

errno = 0;
double valeur = strtod(str, &endPtr);
if (endPtr == str || errno == ERANGE) {
   // Erreur ...
}

N.B. Il faudrait même tester valeur en duo avec errno pour vraiment bien faire.

Conclusion personnelle : oui, sscanf est lourd, très lourd même. Mais l'API est simple, le test d'erreur est simple, ça marche avec tous les formats de données et n'importe quel développeur qui sait faire un printf sait faire une sscanf.

En toute franchise, vous vous voyez écrire ça :

1
2
3
4
5
6
7
char* endPtr;

errno = 0;
long valeur = strtol(str, &endPtr, 10);
if (endPtr == str || errno == ERANGE) {
   // Erreur ...
}

Ou ça :

1
2
3
4
5
6
char c;

long valeur;
if (sscanf(str, "%ld%c", &valeur, &c) != 1) {
    // Erreur ...
}

Pour moi, la seconde solution est beaucoup plus lisible et moins error-prone.


Photo de profil de skywodd

skywodd

Membre

Membre du staff

#110 | Signaler ce message


Même avec toute la bonne volonté du monde, c'est pas encore ça :

 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
// Inclut la bibliothèque standard pour les entrées/sorties en mode terminal
#include <stdio.h>
#include <errno.h>

/** Point d'entré du programme */
int main(int argc, char* argv[]) {

    /* Affiche l'aide si aucun paramètre */
    if (argc == 1) {
        printf("Usage: %s NOMBRE\n", argv[0]);
        puts("Affiche le nombre passé en paramètre sur la sortie standard.");
        return 0;
    }
    
    // Converti le texte en nombre
    char* str = argv[1];
    char* endPtr;
    errno = 0;
    long valeur = strtol(str, &endPtr, 10);
    if (endPtr == str || errno == ERANGE) {
        puts("Erreur: Le paramètre NOMBRE doit être un nombre entier !");
        return 1;
    }

    // Affiche le nombre passé en paramètre
    printf("Vous avez saisi : %ld\n", valeur);

    // Pas d'erreur
    return 0;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
C:\Users\Fabien\Desktop>test.exe 42
Vous avez saisi : 42

C:\Users\Fabien\Desktop>test.exe -17
Vous avez saisi : -17

C:\Users\Fabien\Desktop>test.exe 0
Vous avez saisi : 0

C:\Users\Fabien\Desktop>test.exe azerty
Erreur: Le paramÞtre NOMBRE doit Ûtre un nombre entier !

C:\Users\Fabien\Desktop>test.exe 42azerty
Vous avez saisi : 42

Voila ce que je suis obligé de faire pour avoir le même comportement que "%ld%c" :

 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
// Inclut la bibliothèque standard pour les entrées/sorties en mode terminal
#include <stdio.h>
#include <string.h>
#include <errno.h>

/** Point d'entré du programme */
int main(int argc, char* argv[]) {

    /* Affiche l'aide si aucun paramètre */
    if (argc == 1) {
        printf("Usage: %s NOMBRE\n", argv[0]);
        puts("Affiche le nombre passé en paramètre sur la sortie standard.");
        return 0;
    }
    
    // Converti le texte en nombre
    char* str = argv[1];
    char* endPtr;
    errno = 0;
    long valeur = strtol(str, &endPtr, 10);
    if (endPtr != (str + strlen(str)) || errno == ERANGE) {
        puts("Erreur: Le paramètre NOMBRE doit être un nombre entier !");
        return 1;
    }

    // Affiche le nombre passé en paramètre
    printf("Vous avez saisi : %ld\n", valeur);

    // Pas d'erreur
    return 0;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
C:\Users\Fabien\Desktop>test.exe 42
Vous avez saisi : 42

C:\Users\Fabien\Desktop>test.exe -17
Vous avez saisi : -17

C:\Users\Fabien\Desktop>test.exe 0
Vous avez saisi : 0

C:\Users\Fabien\Desktop>test.exe azerty
Erreur: Le paramÞtre NOMBRE doit Ûtre un nombre entier !

C:\Users\Fabien\Desktop>test.exe 42azerty
Erreur: Le paramÞtre NOMBRE doit Ûtre un nombre entier !

Si j'ai complètement raté un truc, qu'on me le dise, mais en l'état, je ne vois vraiment pas en quoi il est plus simple d'utiliser les fonctions strtoX().