Quelques raccourcis :
Il est facile d'exprimer des horreurs en utilisant des opérations bit à bit...
Notez que les entrées ci-dessous sont écrites comme si char occupait huit bits, sizeof(short)==2 et sizeof(int)==4 ce qui n'est absolument pas garanti par le standard. Adaptez donc le tout en fonction des particularités de vos plateformes respectives.
La situation : supposons qu'on veuille concaténer deux entiers sur huit bits, c0 et c1, dans un seul entier sur 16 bits s0, de manière à ce que c0 soit la partie haute de s0 et c1 soit la partie basse de s0.
Pour régler ce problème, on veut rédiger une fonction nommée concatener_octets() qui prend deux char en paramètre, réalise cette opération de concaténation, et retourne l'entier sur 16 bits résultant (un short). À titre d'exemple, imaginons les valeurs c0 et c1 proposées à droite. On veut manipuler les bits avec des opérateurs bit à bit (glissements et ou bit à bit dans ce cas-ci) pour que s0 devienne un entier équivalent aux valeurs binaires de c0 et de c1 placées côte à côte. |
|
L'horreur : le réflexe (inapproprié) est parfois d'écrire la fonction concatener_octets() comme suit :
short concatener_octets(char c0, char c1)
{ return (c0 << 8) | c1; }
...ce qui signifie :
|
|
Si nous testons notre fonction avec le programme suivant...
#include <iostream>
short concatener_octets(char c0, char c1)
{ return (c0 << 8) | c1; }
int main()
{
using namespace std;
char c0 = 6,
c1 = 7;
cout << concatener_octets(c0, c1) << endl;
}
... alors on obtiendra la bonne réponse c'est -à-dire 1543 ou, si on examine les bits un à un, 0000011000000111 (les zéros à gauche, en italiques ici, ne seraient pas affichés par une calculatrice).
Vous pouvez valider vous-mêmes le tout à l'aide d'une calculatrice comme celle fournie avec à peu près n'importe quelle interface personne/ machine graphique, en examinant l'équivalent binaire de 1543.
Cette fonction est en effet opérationnelle, mais (ce que ne révèle par notre test) seulement pour certains entiers sur huit bits (en fait, pour tous les entiers c0 et c1 inférieurs ou égaux à 127, du moins sur la plupart des compilateurs C et C++).
En effet, le type char de C et de C++ est parfois signé, acceptant les valeurs de -128 à 127 inclusivement, et parfois non signé, acceptant les valeurs de 0 à 255 inclusivement. C'est l'un des rares types des langages C et C++ dont le signe soit dépendant du compilateur (pour short, int et long, par exemple, les entiers sont signés sauf si on ajoute la mention unsigned avant le type). Sur Visual Studio, le type char de C++ est un entier signé sur huit bits. Cela signifie que toute valeur supérieure à 127 et déposée dans un char est considérée comme une valeur négative.
Pour illustrer le problème, reprenons notre programme de test ci-dessus et changeons la valeur de c1 dans l'échantillon de test, y déposant 255 plutôt que 7. Avec la version 2003 française de Visual Studio, ce code de test génère un avertissement (troncation de valeur à l'initialisation de c1), ce qui pourrait nous alerter. Si on ne fait que développer la fonction sans avoir le programme de test tout près, de toute manière, cet avertissement ne nous sera pas fait et nous serons tout de même à risque. Quel est le risque ici? Celui d'un résultat erronné. Si vous essayez le programme à droite, la valeur qui sera affichée sera... -1. |
|
Pourquoi donc?
Le piège est subtil mais il faut examiner la mécanique pour bien le comprendre :
|
|
Le problème apparaît, sous une forme ou l'autre, pour tous les c1 qui ne sont pas entre 0 et 127 inclusivement (et demeure une erreur tactique pour les c0 qui ne sont pas entre 0 et 127 inclusivement).
Le bon usage : quand on manipule des bits, on est habituellement intéressé par un rapport positionnel. On préfère donc, la plupart du temps, travailler avec des entiers non signés et se débarasser entièrement du problème de signe. Ainsi, on peut modifier la fonction comme suit:
short concatener_octets(char c0, char c1)
{ return (static_cast<unsigned char>(c0) << 8) | static_cast<unsigned char>(c1); }
...ou, ce qui serait encore mieux :
unsigned short concatener_octets(char c0, char c1)
{ return (static_cast<unsigned char>(c0) << 8) | static_cast<unsigned char>(c1); }
Ainsi, dans le cas Entrée 00.0 comme dans le cas Entrée 00.1, les entiers sur huit bits manipulés sont traités comme si leur forme binaire désignait un entier non signé, ce qui élimine le problème du négatif imprévu lorsque c1 est transformé en entier sur 16 bits. On peut préférer la seconde forme parce qu'elle permettra à un compilateur strict de générer des erreurs si un appelant tente d'utiliser la valeur retournée par cette fonction d'une manière imprudente, sans empêcher quelque appel que ce soit.
Ce qu'il faut éviter, par contre (à moins que ce ne soit en période de design) est de modifier la signature de la fonction, comme par exemple :
short concatener_octets(unsigned char c0, unsigned char c1)
{ return (c0 << 8) | c1; }
car cela modifierait la convention d'appel et affecterait le code de tous les sous-programmes appelants. Les paramètres d'une fonction font partie de son prototype, mais il en va autrement du type de la valeur retournée.
Reprenons encore une fois notre programme de test ci-dessus avec la valeur 255 pour c1 dans l'échantillon de test. Avec la version 2003 française de Visual Studio, ce code de test générait un avertissement (troncation de valeur à l'initialisation de c1), car la valeur 255 déposée dans c1 demeure problématique (c1 étant signé, on y dépose ainsi encore une fois -1, apparemment sans le vouloir). C'est normal : la forme binaire de -1 sur huit bits signés est la même que la forme binaire de 255 sur huit bits non signés. On pourrait montrer au compilateur qu'on sait ce qu'on fait en remplaçant c1=255 par c1=static_cast<char>(255) et l'avertissement disparaîtrait immédiatement. Cette fois, par contre, le résultat sera celui attendu, soit la valeur 1791 ce qui donne, sous forme binaire, 0000011011111111, donc la concaténation des formes binaires de c0 et de c1. |
|