Les joies des conversions d’entiers en C et C++
Hier, ce blog a eu 7 ans. Quand je l’ai créé, je ne m’étais pas vraiment posé la question de sa durée de vie… Je n’avais sans doute pas pensé que ça durerait aussi longtemps (7 ans donc), que je posterai autant d’articles (142) qui seraient vus par autant de visiteurs (170 812 pour un total de 226 395 vues). Pour fêter cet anniversaire, j’ai choisi un sujet qui a animé de nombreux articles depuis la création de ce blog : le C et ses écueils (et par extension le C++ est concerné). Aujourd’hui, on va parler d’un résultat inattendu d’une division et on va faire un tour de la norme pour tirer les choses au clair !
Question à 1 dollar : que retourne le code suivant ?
int main() {
int a = -6;
unsigned int b = 3;
int c = a / b;
return c;
}
Puisque je pose la question, vous vous doutez bien que ça ne retourne pas -2. Non, sur Windows 10 avec mingw64, ça retourne 1431655763.
Mais alors pourquoi ? Lors de la division, a est promu en unsigned int. Le calcul est donc fait entre deux entiers non signés puis remis dans un signé. Voilà, c’est aussi simple que ça.
Vous n’êtes pas contents ? Vous pensiez que les options -Wall -Wextra verraient ce genre de problèmes ? Je l’ai dit et je le répète encore : ces options sont le minimum vital mais il y en a bien d’autres à activer pour se protéger. Et surtout, c’est un comportement parfaitement normé du langage. Il suffit de se rendre à la section 6.3.1.8 Usual arithmetic conversions de la norme C99 (document n1256) pour y lire :
if the operand that has unsigned integer type has rank greater or
equal to the rank of the type of the other operand, then the operand with
signed integer type is converted to the type of the operand with unsigned
integer type
En fait, il y a moyen d’avoir un avertissement du compilateur. Il suffit de rajouter l’option -Wsign-conversion pour obtenir deux warnings sur la même ligne :
warning: conversion to 'unsigned int' from 'int' may change the sign of the result [-Wsign-conversion] warning: conversion to 'int' from 'unsigned int' may change the sign of the result [-Wsign-conversion]
Le premier correspond à la conversion de b en unsigned int ; le second à la conversion du résultat en int pour l’affecter à c.
cppcheck dit aussi de faire attention :
cppcheck: (warning) Suspicious code: sign conversion of a in calculation, even though a can have a negative value
Notez au passage que a + b, a -b et a * c produisent les bons résultats (au moins sur mon PC, avec ma version de compilateur). Les 4 opérations génèrent des warnings avec GCC mais seules la multiplication et la division génèrent un warning de cppcheck.
PS : merci à Pulkomandy pour m’avoir posé cette colle :p
Caster, c’est mal
En C comme en C++, on se retrouve de temps en temps à caster une variable (de l’anglais to cast ; transtyper en français). Pourtant, caster, c’est mal. Il faut le faire avec parcimonie et avoir de bonnes raisons quand on décide de le faire. Juste « faire taire un warning du compilateur » n’est pas une bonne raison. Quand on caste, on dit au compilateur : « considère cette variable comme étant d’un autre type, ne fais pas de vérification dessus ». Dans un code parfaitement écrit, on en devrait jamais avoir besoin de caster puisque les types seraient toujours bons. La réalité est un peu différente et il faut éviter de cacher les problèmes potentiellement graves.
Voici un premier exemple :
void print(float* p) {
printf("%f", *p);
}
int main() {
int i = 31415;
print(&i);
}
Ce code génère un warning en C :
warning: passing argument 1 of 'print' from incompatible pointer type [-Wincompatible-pointer-types]
print(&i);
^
main.c:3:6: note: expected 'float *' but argument is of type 'int *'
void print(float* p) {
^~~~~
Il génère une erreur en C++ :
error: cannot convert 'int*' to 'float*' for argument '1' to 'void print(float*)'
print(&i);
Dans les 2 cas, caster permet de faire taire le compilateur mais le résultat est bien sûr faux, ça affiche 0.000000. Le cast permet ici de considérer une adresse vers un type comme étant une adresse vers un autre type. On ne change pas la valeur de l’adresse et donc à l’emplacement mémoire, printf() trouve des bits qui correspondent à un entier de 32 bits (probablement codé en complément à 2) et les lit en considérant qu’ils représentent un flottant (logiquement codé en IEEE754). Il n’y a quasiment aucune chance que les 2 représentations donnent la même valeur et l’affichage est faux.
Le problème de ce premier code était assez évident. Prenons un second code un peu plus subtil :
void print(float p) {
printf("%f", p);
}
int main() {
int i = 31415;
print(i);
}
Compilé avec les options -Wall -Wextra, ce code ne génère pas de warning. On pourrait se dire que tout va bien, les langages C et C++ autorisant les conversions entre nombres. Autorisées ne veut pas forcément dire parfaites. Ajoutons l’option -Wconversion pour voir ce que GCC a beau à nous dire :
warning: conversion to 'float' from 'int' may alter its value [-Wconversion] print(i);
Que vérifie exactement ce warning ? Voici sa documentation :
-Wconversion
Warn for implicit conversions that may alter a value. This includes conversions between real and integer, like abs (x) when x is double; conversions between signed and unsigned, like unsigned ui = -1; and conversions to smaller types, like sqrtf (M_PI). Do not warn for explicit casts like abs ((int) x) and ui = (unsigned) -1, or if the value is not changed by the conversion like in abs (2.0). Warnings about conversions between signed and unsigned integers can be disabled by using -Wno-sign-conversion.For C++, also warn for confusing overload resolution for user-defined conversions; and conversions that never use a type conversion operator: conversions to void, the same type, a base class or a reference to them. Warnings about conversions between signed and unsigned integers are disabled by default in C++ unless -Wsign-conversion is explicitly enabled.
Bien sûr, un cast empêche le warning d’apparaître. Warning ou pas, la sortie console est bien 31415.000000. En revanche, si i = 987654321, alors la sortie devient 987654336.000000… Et de manière marrante, si i = 987654336 alors la sortie est bien 987654336.000000 et est donc correcte. Vous êtes surpris qu’un code qui passe bien avec -Wall -Wextra puisse produire des résultats erronés ? Hé hé… Premièrement, ces deux options sont le minimum vital et vous pouvez (devez ?) en ajouter d’autres, par exemple -Wwrite-strings. Deuxièmement, les conversions entre nombres, que ce soit lors de simples affectations ou lors de calculs, réservent bien des surprises et vous devriez vous méfiez. -Wconversion est fait pour ça ; -Wsign-conversion et -Wfloat-conversion sont un peu moins brutaux (ils sont activés par -Wconversion automatiquement). Il existe aussi -Wdouble-promotion qui peut révéler des possibles pertes de performances.
Ici, ce n’est que de l’affichage, ce n’est pas catastrophique, mais si c’était des calculs pour la stabilisation votre drône DIY, vous rigoleriez moins ! ; )
Voici un dernier exemple bien plus violent :
void modify(char* p) {
p[0] += 1;
}
int main() {
char* message = (char*) "hello";
modify(message);
}
Ce code cache un warning en C++ :
warning: ISO C++ forbids converting a string constant to 'char*' [-Wwrite-strings]
char* message = "hello";
^~~~~~~
En C, il faut rajouter l’option explicitement car elle n’est pas inclus dans -Wall -Wextra. Que se passe t-il si on exécute ce code ? Une erreur de segmentation, tout simplement, puisque le programme va tenter d’écrire dans une zone mémoire qui est en lecture seule.
Cet article a prouvé au passage que le système de typage est plus fort en C++ qu’en C mais ce n’était pas son but premier. Le but était de vous montrer que le système de typage est là pour vous aider et que caster vous laisse seul face à vos erreurs au lieu de bénéficier de l’aide de votre compilateur. Rares sont les codes C et C++ sans cast, surtout si vous faites joujou avec du code très bas niveau. Essayez toujours de ne pas avoir à caster, utiliser les opérateur de cast en C++ pour expliciter le but du cast, méfiez-vous des conversions implicites et activer les options de votre compilateur pour qu’il vous aide à détecter autant d’erreurs que possibles.


