Pourquoi les résultats de la promotion des entiers sont-ils différents?

S’il vous plaît regardez mon code de test:

#include  #include  #define PRINT_COMPARE_RESULT(a, b) \ if (a > b) { \ printf( #a " > " #b "\n"); \ } \ else if (a < b) { \ printf( #a " < " #b "\n"); \ } \ else { \ printf( #a " = " #b "\n" ); \ } int main() { signed int a = -1; unsigned int b = 2; signed short c = -1; unsigned short d = 2; PRINT_COMPARE_RESULT(a,b); PRINT_COMPARE_RESULT(c,d); return 0; } 

Le résultat est le suivant:

 a > b c < d 

Ma plate-forme est Linux et ma version de gcc est 4.4.2. Je suis surpris par la deuxième ligne de sortie. La première ligne de sortie est provoquée par une promotion entière. Mais pourquoi le résultat de la deuxième ligne est-il différent?

Les règles suivantes sont issues de la norme C99:

Si les deux opérandes ont le même type, aucune conversion supplémentaire n’est nécessaire. Sinon, si les deux opérandes ont des types entiers signés ou que les deux ont des types entiers non signés, l’opérande ayant le type de rang de conversion de nombre entier inférieur est converti en type de l’opérande avec le rang le plus élevé.

Sinon, si l’opérande qui a un type entier non signé a un rang supérieur ou égal au rang du type de l’autre opérande, l’opérande avec le type entier signé est converti au type de l’opérande avec le type entier non signé.

Sinon, si le type de l’opérande avec le type entier signé peut représenter toutes les valeurs du type de l’opérande avec le type entier non signé, l’opérande avec le type entier non signé est converti en type de l’opérande avec le type entier signé.

Sinon, les deux opérandes sont convertis en un type entier non signé correspondant au type de l’opérande avec un type entier signé.

Je pense que les deux comparaisons devraient appartenir au même cas, le deuxième cas de promotion entière.

Lorsque vous utilisez un opérateur arithmétique, les opérandes effectuent deux conversions.

Promotions entières: Si int peut représenter toutes les valeurs du type, l’opérande est alors promu int. Ceci s’applique à short et à unsigned short sur la plupart des plates-formes. La conversion effectuée sur cette étape est effectuée sur chaque opérande individuellement, sans tenir compte de l’autre opérande. (Il y a plus de règles, mais c’est celle qui s’applique.)

Conversions arithmétiques habituelles: Si vous comparez un unsigned int rapport à un unsigned int signed int , puisque ni l’une ni l’autre ne comprend la plage entière de l’autre et que les deux ont le même rang, elles sont toutes deux converties en type unsigned . Cette conversion est effectuée après avoir examiné le type des deux opérandes.

Évidemment, les “conversions arithmétiques habituelles” ne s’appliquent pas toujours, s’il n’y a pas deux opérandes. C’est pourquoi il existe deux ensembles de règles. Un des pièges, par exemple, est que les opérateurs de décalage << et >> ne font pas les conversions arithmétiques habituelles, car le type du résultat ne devrait dépendre que de l'opérande gauche (ainsi, si vous voyez quelqu'un taper x << 5U , alors U signifie "inutile").

Panne: supposons un système typique avec int de 32 bits et court de 16 bits.

 int a = -1; // "signed" is implied unsigned b = 2; // "int" is implied if (a < b) puts("a < b"); // not printed else puts("a >= b"); // printed 
  1. Les deux opérandes sont d'abord promus. Puisque les deux sont int ou unsigned int , aucune promotion n'est faite.
  2. Ensuite, les deux opérandes sont convertis dans le même type. Puisque int ne peut pas représenter toutes les valeurs possibles de unsigned , et unsigned ne peut pas représenter toutes les valeurs possibles de int , il n'y a pas de choix évident. Dans ce cas, les deux sont convertis en unsigned .
  3. Lors de la conversion de signé en non signé, 2 32 sont ajoutés de manière répétée à la valeur signée jusqu'à ce qu'ils se trouvent dans la plage de la valeur non signée. C'est en fait un noop en ce qui concerne le processeur.
  4. La comparaison devient donc if (4294967295u < 2u) , ce qui est faux.

Essayons maintenant avec short :

 short c = -1; // "signed" is implied unsigned short d = 2; if (c < d) puts("c < d"); // printed else puts("c >= d"); // not printed 
  1. Premièrement, les deux opérandes sont promus. Puisque les deux peuvent être représentés fidèlement par int , ils sont tous deux promus int .
  2. Ensuite, ils sont convertis dans le même type. Mais ils sont déjà du même type, int , donc rien n’est fait.
  3. La comparaison devient donc if (-1 < 2) , ce qui est vrai.

Écrire un bon code: Il existe un moyen facile d’attraper ces "pièges" dans votre code. Comstackz toujours avec les avertissements activés et corrigez les avertissements. J'ai tendance à écrire un code comme celui-ci:

 int x = ...; unsigned y = ...; if (x < 0 || (unsigned) x < y) ...; 

Vous devez faire attention à ce que le code que vous écrivez ne se retrouve pas dans l'autre Gotcha signé et non signé: débordement signé. Par exemple, le code suivant:

 int x = ..., y = ...; if (x + 100 < y + 100) ...; unsigned a = ..., b = ...; if (a + 100 < b + 100) ...; 

Certains compilateurs populaires vont optimiser (x + 100 < y + 100) à (x < y) , mais c'est une histoire pour un autre jour. Juste ne débordez pas vos numéros signés.

Note de bas de page: Notez que bien que signed soit implicite pour int , short , long et long long , il n'est PAS implicite pour char . Au lieu de cela, cela dépend de la plate-forme.

Tiré du standard C ++:

4.5 Promotions intégrales [conv.prom]
1 Une rvalue de type char, char signée, unsigned char, short int ou unsigned short int peut être convertie en une valeur de type int si si int peut représenter toutes les valeurs du type source; sinon, la rvalue source peut être convertie en une rvalue de type unsigned int.

En pratique, cela signifie que toutes les opérations (sur les types de la liste) sont réellement évaluées sur le type int si celui-ci peut couvrir l’ensemble du jeu de valeurs que vous traitez, sinon il est exécuté sur unsigned int . Dans le premier cas, les valeurs sont comparées avec unsigned int parce que l’un d’eux était unsigned int et c’est pourquoi -1 est “supérieur” à 2. Dans le second cas, les valeurs sont comparées en tant qu’entiers signés, car int couvre tout le domaine de short et unsigned short et donc -1 est inférieur à 2.

(Histoire de base: En fait, toute cette définition complexe de couvrir tous les cas de cette manière a pour conséquence que les compilateurs peuvent ignorer le type réel derrière (!) 🙂 et se soucier de la taille des données.)

Le processus de conversion pour C ++ est décrit comme les conversions arithmétiques habituelles . Cependant, je pense que la règle la plus pertinente est dans la section sous-référencée conv.prom: promotions intégrales 4.6.1 :

Une valeur d’un type entier autre que bool, char16_t, char32_t ou wchar_t dont le rang de conversion d’entier ([conv.rank]) est inférieur au rang d’int peut être converti en une valeur de type int si int peut représenter toutes les valeurs du type source; sinon, la prvalue source peut être convertie en une valeur de type unsigned int.

La chose amusante est l’utilisation du mot “can”, ce qui suggère que cette promotion est effectuée à la discrétion du compilateur.

J’ai aussi trouvé cet extrait de code C qui fait allusion à l’omission de la promotion:

 11 EXAMPLE 2 In executing the fragment char c1, c2; /* ... */ c1 = c1 + c2; the ``integer promotions'' require that the abstract machine promote the value of each variable to int size and then add the two ints and truncate the sum. Provided the addition of two chars can be done without overflow, or with overflow wrapping silently to produce the correct result, the actual execution need only produce the same result, possibly omitting the promotions. 

Il y a aussi la définition de “rang” à prendre en compte. La liste des règles est assez longue, mais comme elle s’applique à cette question, le “rang” est simple:

Le rang de tout type entier non signé doit être égal au rang du type entier signé signé.