Notes sur les entiers non signés

Les entiers non signés sont des amis dangereux. Rarement aura-t-on vu autant de bugs causés par un seul responsable. Voici quelques notes sur le sujet.

La boucle

Considérons cette boucle d'apparence bénigne:

for (unsigned int i = 10; i >= 0; --i) { /* ... */ }

Très peu de programmeurs seront en mesure d'apprécier, dès le premier coup d'œil, que cette boucle ne terminera jamais. En effet, l'entier non signé utilisé comme indice va déborder à 0xffffffff avant de pouvoir invalider la condition d'arrêt de la boucle. Évidemment, l'utilisation d'un entier signé règle ce problème instantanément.

for (/* unsigned */ int i = 10; i >= 0; --i) { /* ... */ }

Pour se consoler, on peut toujours se dire qu'une boucle infinie termine beaucoup plus rapidement en C++ qu'en Python — consolation de courte durée.

La comparaison

Les comparaisons entre des entiers signés et non signés se comportent parfois de manière inattendue. Considérons l'exemple ci-dessous.

int a = -1;
unsigned int b = 1;

cout << (a > b ? "-1 > 1": "-1 < 1") << endl;

Étrangement, ce code affichera «-1 > 1». Ce résultat surprenant est dû à la conversion, tout juste avant que la comparaison ne soit effectuée, de l'entier signé en entier non signé. Pour comprendre la situation, il faut savoir que la représentation binaire en complément 2 de -1 est 0xffffffff. Or, ce même patron de bits, lorsqu'il est traité comme un entier non signé, représente plutôt une valeur de 4294967295. Le bit de signe signe perd tout simplement sa signification.

L'interface

Les interfaces employant les entiers non signés sont également sujettes à cette même famille de bugs.

Il y a de cela plusieurs années, Scott Meyers a d'ailleurs écrit deux articles sur le sujet. Parus dans le C++ Report, ceux-ci s'intitulaient Signed and Unsigned Types in Interfaces et Interface Types Revisited.

Pour comprendre la nature du problème, considérons une fonction prenant en argument un entier non signé.

void f(unsigned int i) {
    /* ... */
}

Maintenant, supposons que la valeur passée à la fonction est négative; par exemple -1. Tel que mentionné précédemment, une conversion de l'entier signé vers l'entier non signé aura lieu. Lors de celle-ci, le bit de signe perdra sa signification et la variable i prendra alors comme valeur 4294967295.

S'il n'y a pas de solution véritablement élégante à ce problème, il est tout de même possible d'en réduire les conséquences. En particulier, si le paramètre de la fonction n'a pas à employer toute la plage de valeurs des entiers non signés, il est possible d'insérer une vérification au début de la fonction.

void f(unsigned int i) {
    if (static_cast<int>(i) > numeric_limits<int>::max())
        throw runtime_error("negative value");

    /* ... */
}

Évidemment, la plage de valeurs acceptée par la fonction est réduite de moitié. Toutefois, on conserve une interface spécifiant explicitement qu'une valeur non signée est attendue.

template <typename Signed, typename Unsigned>
bool wrap_from_negative(Unsigned i) {
    return static_cast<Signed>(i) > numeric_limits<Signed>::max();
}

void f(unsigned int i) {
    if (wrap_from_negative<int>(i))
        throw runtime_error("negative value");

    /* ... */
}

La solution demeure tout de même peu élégante.

Conclusion

Les entiers non signés sont donc des amis dangereux. Lorsque le seul motif justifiant leur emploi est la lisibilité, le jeu n’en vaut probablement pas la chandelle.

Google à d'ailleurs reconnu ce risque et décourage officiellement l'utilisation des entiers non signés dans ses normes de programmation.

Un commentaire dans le code de Google Chrome (base/basictypes.cpp) laisse d'ailleurs peu de place à l'interprétation.

// NOTE: unsigned types are DANGEROUS in loops and other arithmetical
// places.  Use the signed types unless your variable represents a bit
// pattern (eg a hash value) or you really need the extra bit.  Do NOT
// use 'unsigned' to express "this value should always be positive";
// use assertions for this.

Finalement, le compilateur est généralement de bon conseil. GCC génère des avertissements pour les exemples présentés dans ce texte lorsque les options -Wall et -Wextra sont employées conjointement.

$ g++ -Wall -Wextra compare.cpp -o compare
compare.cpp: In function ‘int main()’:
compare.cpp:16:18: attention : comparaison entre des expressions entières signée et non signée
$ g++ -Wall -Wextra loop.cpp -o loop
loop.cpp: In function ‘int main()’:
loop.cpp:14:35: attention : comparaison d'une expression non signée >=0 est toujours vraie

Le code source des programmes exhibant les comportements mentionnés précédemment peut être consulté en ligne.