C a été conçu pour changer implicitement et silencieusement les types d'entiers des opérandes utilisés dans les expressions. Il existe plusieurs cas où le langage force le compilateur à changer les opérandes vers un type plus grand, ou à changer leur signature.
Le but est d'éviter les débordements accidentels pendant l'arithmétique, mais aussi de permettre à des opérandes de signe différent de coexister dans la même expression.
Malheureusement, les règles de promotion de type implicite causent beaucoup plus de mal que de bien, au point qu'elles pourraient être l'un des plus grands défauts du langage C. Ces règles ne sont souvent même pas connues du programmeur C moyen et provoquent donc toutes sortes de bogues très subtils.
En général, on voit des scénarios où le programmeur dit "il suffit de faire un casting avec le type x et ça marche" - mais il ne sait pas pourquoi. Ou encore, ces bogues se manifestent sous la forme d'un phénomène rare et intermittent dans un code apparemment simple et direct. La promotion implicite est particulièrement gênante dans le code faisant des manipulations de bits, puisque la plupart des opérateurs de type bit-wise en C ont un comportement mal défini lorsqu'ils reçoivent un opérande signé.
Types de nombres entiers et rang de conversion
Les types d'entiers en C sont char
, short
, int
, long
, long long
y enum
.
_Bool
/ bool
est également traité comme un type entier lorsqu'il s'agit de promotions de type.
Tous les nombres entiers ont une valeur rang de conversion . C11 6.3.1.1, j'insiste sur les parties les plus importantes :
Chaque type d'entier possède un rang de conversion d'entier défini comme suit :
- Deux types d'entiers signés ne doivent pas avoir le même rang, même s'ils ont la même représentation.
- Le rang d'un type d'entier signé doit être supérieur au rang de tout type d'entier signé de moindre précision.
- Le rang de long long int
est supérieur au rang de long int
qui doit être supérieur au rang de int
qui doit être supérieur au rang de short int
qui doit être supérieur au rang de signed char
.
- Le rang de tout type d'entier non signé doit être égal au rang du type d'entier signé correspondant, le cas échéant.
- Le rang de tout type d'entier standard doit être supérieur au rang de tout type d'entier étendu de même largeur.
- Le rang des caractères doit être égal au rang des caractères signés et des caractères non signés.
- Le rang de _Bool doit être inférieur à celui de tous les autres types d'entiers standard.
- Le rang de tout type énuméré doit être égal au rang du type entier compatible (voir 6.7.2.2).
Les types de stdint.h
sont également classés ici, avec le même rang que le type auquel ils correspondent sur le système donné. Par exemple, int32_t
a le même rang que int
sur un système 32 bits.
En outre, l'article C11 6.3.1.1 précise les types qui sont considérés comme les types de petits nombres entiers (terme non officiel) :
Les éléments suivants peuvent être utilisés dans une expression chaque fois qu'un élément int
o unsigned int
mai être utilisés :
- Un objet ou une expression avec un type entier (autre que int
o unsigned int
) dont le rang de conversion en entier est inférieur ou égal au rang de int
y unsigned int
.
Ce que ce texte quelque peu cryptique signifie en pratique, c'est que _Bool
, char
y short
(et aussi int8_t
, uint8_t
etc) sont les "petits types entiers". Ceux-ci sont traités de manière spéciale et soumis à une promotion implicite, comme expliqué ci-dessous.
Les promotions entières
Chaque fois qu'un petit type d'entier est utilisé dans une expression, il est implicitement converti en int
qui est toujours signé. C'est ce que l'on appelle le promotions entières o la règle de promotion des nombres entiers .
Formellement, la règle dit (C11 6.3.1.1) :
Si un int
peut représenter toutes les valeurs du type original (limité par la largeur, pour un champ de bits), la valeur est convertie en un champ int
; sinon, il est converti en un unsigned int
. On les appelle les promotions entières .
Cela signifie que tous les petits types d'entiers, quel que soit leur signe, sont implicitement convertis en (signés) int
lorsqu'il est utilisé dans la plupart des expressions.
Ce texte est souvent compris à tort comme : "tous les petits types d'entiers signés sont convertis en int signés et tous les petits types d'entiers non signés sont convertis en int non signés". Ceci est incorrect. La partie non signée signifie seulement que si nous avons, par exemple, un type unsigned short
l'opérande, et int
se trouve avoir la même taille que short
sur le système donné, alors le unsigned short
est converti en unsigned int
. Comme dans, rien de notable ne se passe vraiment. Mais au cas où short
est un type plus petit que int
il est toujours converti en (signé) int
, que le raccourci soit signé ou non signé. !
La dure réalité causée par les promotions d'entiers signifie que presque aucune opération en C ne peut être effectuée sur de petits types comme char
o short
. Les opérations sont toujours effectuées sur int
ou des types plus grands.
Cela peut sembler absurde, mais heureusement, le compilateur est autorisé à optimiser le code. Par exemple, une expression contenant deux unsigned char
les opérandes seraient promus au rang de int
et l'opération effectuée comme int
. Mais le compilateur est autorisé à optimiser l'expression pour qu'elle soit effectivement exécutée comme une opération à 8 bits, comme on pourrait s'y attendre. Cependant, voici le problème : le compilateur est pas est autorisé à optimiser le changement implicite de signe causé par la promotion des entiers. Parce qu'il n'y a aucun moyen pour le compilateur de dire si le programmeur compte délibérément sur la promotion implicite pour se produire, ou si c'est involontaire.
C'est pourquoi l'exemple 1 de la question échoue. Les deux opérandes unsigned char sont promus au type int
l'opération s'effectue sur le type int
et le résultat de x - y
est de type int
. Ce qui signifie que nous obtenons -1
au lieu de 255
ce qui aurait pu être attendu. Le compilateur peut générer un code machine qui exécute le code avec des instructions de 8 bits au lieu des instructions de 10 bits. int
mais il ne peut pas optimiser le changement de signature. Ce qui signifie que nous nous retrouvons avec un résultat négatif, qui à son tour donne un nombre bizarre lorsque printf("%u
est invoquée. L'exemple 1 pourrait être corrigé en coulant le résultat de l'opération vers le type unsigned char
.
À l'exception de quelques cas particuliers comme ++
y sizeof
les promotions d'entiers s'appliquent à presque toutes les opérations en C, peu importe si des opérateurs unaires, binaires (ou ternaires) sont utilisés.
Les conversions arithmétiques habituelles
Lorsqu'une opération binaire (une opération avec 2 opérandes) est effectuée en C, les deux opérandes de l'opérateur doivent être du même type. Par conséquent, si les opérandes sont de types différents, le C applique une conversion implicite d'un opérande au type de l'autre opérande. Les règles qui régissent cette opération sont nommées les conversions arithmétiques habituelles (parfois appelé de manière informelle "équilibrage"). Ils sont spécifiés dans l'article C11 6.3.18 :
(Pensez à cette règle comme à une longue règle imbriquée. if-else if
et il sera peut-être plus facile à lire :) )
6.3.1.8 Conversions arithmétiques habituelles
De nombreux opérateurs qui attendent des opérandes de type arithmétique provoquent des conversions et produisent des résultats d'une manière similaire. Le but est de déterminer un type réel commun pour les opérandes et le résultat. Pour les opérandes spécifiés, chaque opérande est converti, sans changement de domaine de type domaine, en un type dont le type réel correspondant est le type réel commun. Sauf si Sauf indication contraire explicite, le type réel commun est également le type réel correspondant du résultat, dont le domaine de type est le même que celui du résultat. le résultat, dont le domaine de type est le domaine de type des opérandes s'ils sont identiques, et complexe sinon. Ce modèle est appelé les conversions arithmétiques habituelles :
Il convient de noter que les conversions arithmétiques habituelles s'appliquent à la fois aux variables à virgule flottante et aux variables entières. Dans le cas des entiers, nous pouvons également noter que les promotions d'entiers sont invoquées à partir des conversions arithmétiques habituelles. Et ensuite, lorsque les deux opérandes ont au moins le rang de int
les opérateurs sont équilibrés au même type, avec la même signature.
C'est la raison pour laquelle a + b
dans l'exemple 2 donne un résultat étrange. Les deux opérandes sont des entiers et ils sont au moins de rang int
Les promotions sur les nombres entiers ne s'appliquent donc pas. Les opérandes ne sont pas du même type. a
est unsigned int
y b
est signed int
. Par conséquent, l'opérateur b
est temporairement converti en type unsigned int
. Au cours de cette conversion, il perd l'information du signe et se retrouve sous la forme d'une grande valeur.
La raison pour laquelle le changement de type en short
dans l'exemple 3 résout le problème, c'est parce que short
est un petit type d'entier. Ce qui signifie que les deux opérandes sont des entiers promus au type int
qui est signé. Après la promotion des entiers, les deux opérandes ont le même type ( int
), aucune autre conversion n'est nécessaire. Et alors l'opération peut être effectuée sur un type signé comme prévu.
3 votes
Je suggère de documenter les hypothèses pour les exemples, par exemple, l'exemple 3 suppose que
short
est plus étroite queint
(ou en d'autres termes, elle suppose queint
peut représenter toutes les valeurs deunsigned short
).0 votes
Attendez une seconde, l'OP est le même gars qui a répondu à la question ? Il est dit que Lundin a demandé, la meilleure réponse est aussi celle de Lundin lol.
4 votes
@savram Oui, l'intention est d'écrire une entrée de FAQ. Le partage des connaissances de cette manière convient à l'OS - la prochaine fois que vous posez une question, notez la case à cocher "répondre à votre propre question". Mais bien sûr, la question est toujours traitée comme n'importe quelle autre question et d'autres peuvent également publier des réponses. (Et vous ne gagnez pas de réputation en acceptant votre propre réponse).
0 votes
@savram : Il est tout à fait correct de partager les connaissances de cette manière. Voir ici : auto-réponse .
3 votes
Aucune des réponses ne mentionne jusqu'à présent le fait que
printf("%u\n", x - y);
provoque un comportement indéfini0 votes
@M.M. Je l'ai changé en
%d
heureux maintenant ? De plus, je préfère ne pas glisser les étranges "promotions d'arguments par défaut" dans ce billet, elles sont laissées de côté exprès, pour ne pas embrouiller le lecteur avec des règles de langage d'intérêt secondaire.0 votes
@Lundin Non, pas heureux maintenant. Le premier extrait de code après votre modification imprimera
-1
sur la plupart des implémentations, mais vous vous demandez "Pourquoi cela donne-t-il un grand nombre entier étrange et non 255 ?". Ceci est pertinent pour le sujet parce que c'est une sorte d'impasse : vous ne pouvez pas savoir quel spécificateur de format utiliser tant que vous ne comprenez pas les règles de promotion.2 votes
Un bon exemple est
~((u8)(1 << 7))
à la liste.