web-dev-qa-db-fra.com

Règles de promotion de type implicite

Ce message est destiné à être utilisé comme un FAQ concernant la promotion d'entier implicite en C, en particulier la promotion implicite causée par les conversions arithmétiques habituelles et/ou les promotions d'entier.

Exemple 1)
Pourquoi cela donne-t-il un nombre entier étrange et non 255?

unsigned char x = 0;
unsigned char y = 1;
printf("%u\n", x - y); 

Exemple 2)
Pourquoi cela donne-t-il "-1 est supérieur à 0"?

unsigned int a = 1;
signed int b = -2;
if(a + b > 0)
  puts("-1 is larger than 0");

Exemple 3)
Pourquoi changer le type dans l'exemple ci-dessus en short résout le problème?

unsigned short a = 1;
signed short b = -2;
if(a + b > 0)
  puts("-1 is larger than 0"); // will not print

(Ces exemples étaient destinés à un ordinateur 32 ou 64 bits avec 16 bits courts.)

45
Lundin

C a été conçu pour changer implicitement et silencieusement les types entiers des opérandes utilisés dans les expressions. Il existe plusieurs cas où le langage force le compilateur à changer les opérandes en un type plus grand, ou à changer leur signature.

La raison derrière cela est d'empêcher les débordements accidentels pendant l'arithmétique, mais aussi de permettre aux opérandes de signature différente de coexister dans la même expression.

Malheureusement, les règles pour la promotion de type implicite causent beaucoup plus de tort que de bien, au point où elles pourraient être l'un des plus gros 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 bugs très subtils.

En règle générale, vous voyez des scénarios où le programmeur dit "simplement transtypé en type x et cela fonctionne" - mais ils ne savent pas pourquoi. Ou de tels bogues se manifestent comme un phénomène rare et intermittent venant de l'intérieur d'un code apparemment simple et direct. La promotion implicite est particulièrement gênante dans le code faisant des manipulations de bits, car la plupart des opérateurs au niveau du bit en C ont un comportement mal défini lorsqu'ils reçoivent un opérande signé.


types entiers et rang de conversion

Les types entiers en C sont char, short, int, long, long long et enum.
_Bool/bool est également traité comme un type entier lorsqu'il s'agit de promotions de type.

Tous les entiers ont un rang de conversion spécifié . C11 6.3.1.1, je mets l'accent sur les parties les plus importantes:

Chaque type entier a un rang de conversion 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 entier signé doit être supérieur au rang de tout type entier signé avec moins de précision.
- Le rang de long long int doit être 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 entier non signé doit être égal au rang du type entier signé correspondant, le cas échéant.

- Le rang de tout type entier standard doit être supérieur au rang de tout type entier étendu de même largeur.
.
- Le rang de _Bool doit être inférieur au rang de tous les autres types 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 triez ici aussi, 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.

De plus, C11 6.3.1.1 spécifie les types qui sont considérés comme les petits types entiers (pas un terme formel):

Les éléments suivants peuvent être utilisés dans une expression partout où un int ou unsigned int peut être utilisé:

- Un objet ou une expression avec un type entier (autre que int ou unsigned int) dont le rang de conversion entier est inférieur ou égal au rang de int et unsigned int.

Ce que ce texte quelque peu cryptique signifie dans la pratique, c'est que _Bool, char et 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 entier est utilisé dans une expression, il est implicitement converti en int qui est toujours signé. Ceci est connu sous le nom de promotions entières ou la règle de promotion entière .

Formellement, la règle dit (C11 6.3.1.1):

Si un int peut représenter toutes les valeurs du type d'origine (limité par la largeur, pour un champ de bits), la valeur est convertie en int; sinon, il est converti en unsigned int. Celles-ci sont appelées promotions entières .

Cela signifie que tous les petits types entiers, quelle que soit la signature, sont implicitement convertis en (signé) int lorsqu'ils sont utilisés dans la plupart des expressions.

Ce texte est souvent mal compris: "tous les petits types entiers signés sont convertis en entier signé et tous les petits types entiers non signés sont convertis en entier non signé". Ceci est une erreur. La partie non signée ici signifie seulement que si nous avons par exemple un unsigned short opérande et int se trouve avoir la même taille que short sur le système donné, puis le unsigned short l'opérande est converti en unsigned int. Comme dans, rien de remarquable ne se passe vraiment. Mais dans le cas où short est un type plus petit que int, il est toujours converti en (signé) int, quel que soit le court a été signé ou non signé !

La dure réalité causée par les promotions entières signifie que presque aucune opération en C ne peut être effectuée sur de petits types comme char ou short. Les opérations sont toujours effectuées sur int ou sur 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 obtiendraient les opérandes promus en int et l'opération effectuée en tant que int. Mais le compilateur est autorisé à optimiser l'expression pour qu'elle soit réellement exécutée comme une opération 8 bits, comme on pourrait s'y attendre. Cependant, voici le problème: le compilateur n'est pas autorisé à optimiser le changement implicite de signature provoqué par la promotion d'entiers. Parce qu'il n'y a aucun moyen pour le compilateur de savoir si le programmeur s'appuie délibérément sur une promotion implicite pour se produire, ou si elle n'est pas intentionnelle.

C'est pourquoi l'exemple 1 de la question échoue. Les deux opérandes char non signés sont promus au type int, l'opération est effectuée sur le type int, et le résultat de x - y est de type int. Cela signifie que nous obtenons -1 au lieu de 255 qui aurait pu être attendu. Le compilateur peut générer du code machine qui exécute le code avec des instructions 8 bits au lieu de int, mais il ne peut pas optimiser le changement de signature. Cela signifie que nous nous retrouvons avec un résultat négatif, ce qui entraîne à son tour un nombre étrange lorsque printf("%u est invoqué. L'exemple 1 peut être corrigé en redirigeant le résultat de l'opération vers le type unsigned char.

À l'exception de quelques cas spéciaux comme ++ et sizeof, les promotions entières 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

Chaque fois qu'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, dans le cas où les opérandes sont de types différents, C impose une conversion implicite d'un opérande au type de l'autre opérande. Les règles pour ce faire sont nommées les conversions artihmétiques habituelles (parfois appelées de manière informelle "équilibrage"). Ceux-ci sont spécifiés en C11 6.3.18:

(Considérez cette règle comme une longue, imbriquée if-else if déclaration et il pourrait ê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 types de résultats de 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, en un type dont le type réel correspondant est le type réel commun. 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 domaine de type des opérandes s'ils sont identiques, et complexe sinon. Ce modèle est appelé les conversions arithmétiques habituelles :

  • Tout d'abord, si le type réel correspondant de l'un ou l'autre opérande est long double, l'autre opérande est converti, sans changement de domaine de type, en un type dont le type réel correspondant est long double.
  • Sinon, si le type réel correspondant de l'un des opérandes est double, l'autre opérande est converti, sans changement de domaine de type, en un type dont le type réel correspondant est double.
  • Sinon, si le type réel correspondant de l'un des opérandes est float, l'autre opérande est converti, sans changement de domaine de type, en un type dont le type réel correspondant est float.
  • Sinon, les promotions entières sont effectuées sur les deux opérandes. Ensuite, les règles suivantes sont appliquées aux opérandes promus:

    • 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 les deux ont des types entiers non signés, l'opérande avec le type de rang de conversion d'entier inférieur est converti en type d'opérande avec un rang plus élevé.
    • Sinon, si l'opérande qui a le type entier non signé a un rang supérieur ou égal au rang du type de l'autre opérande, alors l'opérande avec le type entier signé est converti en type de l'opérande avec le type entier non signé.
    • Sinon, si le type de l'opérande de type entier signé peut représenter toutes les valeurs du type de l'opérande de type entier non signé, alors l'opérande de type entier non signé est converti en type d'opérande de 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é.

Il convient de noter ici que les conversions arithmétiques habituelles s'appliquent aux variables à virgule flottante et aux variables entières. Dans le cas d'entiers, on peut également noter que les promotions d'entiers sont invoquées à partir des conversions arithmétiques habituelles. Et après cela, lorsque les deux opérandes ont au moins le rang de int, les opérateurs sont équilibrés sur le même type, avec la même signature.

C'est la raison pourquoi 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, donc les promotions entières ne s'appliquent pas. Les opérandes ne sont pas du même type - a is unsigned int et 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 les informations de signe et se retrouve comme une grande valeur.

La raison pour laquelle changer le type en short dans l'exemple 3 résout le problème, parce que short est un petit type entier. Cela signifie que les deux opérandes sont des nombres entiers promus au type int qui est signé. Après la promotion d'entiers, les deux opérandes ont le même type (int), aucune conversion supplémentaire n'est nécessaire. Et puis l'opération peut être effectuée sur un type signé comme prévu.

55
Lundin

Selon le post précédent, je veux donner plus d'informations sur chaque exemple.

Exemple 1)

int main(){
    unsigned char x = 0;
    unsigned char y = 1;
    printf("%u\n", x - y); 
    printf("%d\n", x - y);
}

Étant donné que le caractère non signé est plus petit que int, nous leur appliquons la promotion entière, puis nous avons (int) x- (int) y = (int) (- 1) et unsigned int (-1) = 4294967295.

La sortie du code ci-dessus: (identique à ce que nous attendions)

4294967295
-1

Comment y remédier?

J'ai essayé ce que le post précédent recommandait, mais cela ne fonctionne pas vraiment. Voici le code basé sur le post précédent:

changez l'un d'eux en entier non signé

int main(){
    unsigned int x = 0;
    unsigned char y = 1;
    printf("%u\n", x - y); 
    printf("%d\n", x - y);
}

Puisque x est déjà un entier non signé, nous appliquons uniquement la promotion d'entier à y. Ensuite, nous obtenons (unsigned int) x- (int) y. Puisqu'ils n'ont toujours pas le même type, nous appliquons les conversions arithmétiques habituelles, nous obtenons (unsigned int) x- (unsigned int) y = 4294967295.

La sortie du code ci-dessus: (identique à ce que nous attendions):

4294967295
-1

De même, le code suivant obtient le même résultat:

int main(){
    unsigned char x = 0;
    unsigned int y = 1;
    printf("%u\n", x - y); 
    printf("%d\n", x - y);
}

changez les deux en entier non signé

int main(){
    unsigned int x = 0;
    unsigned int y = 1;
    printf("%u\n", x - y); 
    printf("%d\n", x - y);
}

Étant donné que les deux ne sont pas signés int, aucune promotion entière n'est nécessaire. Par la convergence arithmétique habituelle (ont le même type), (entier non signé) x- (entier non signé) y = 4294967295.

La sortie du code ci-dessus: (identique à ce que nous attendions):

4294967295
-1

Une des façons possibles de corriger le code: (ajoutez un type cast à la fin)

int main(){
    unsigned char x = 0;
    unsigned char y = 1;
    printf("%u\n", x - y); 
    printf("%d\n", x - y);
    unsigned char z = x-y;
    printf("%u\n", z);
}

La sortie du code ci-dessus:

4294967295
-1
255

Exemple 2)

int main(){
    unsigned int a = 1;
    signed int b = -2;
    if(a + b > 0)
        puts("-1 is larger than 0");
        printf("%u\n", a+b);
}

Étant donné que les deux sont des entiers, aucune promotion d'entier n'est nécessaire. Par la conversion arithmétique habituelle, nous obtenons (entier non signé) a + (entier non signé) b = 1 + 4294967294 = 4294967295.

La sortie du code ci-dessus: (identique à ce que nous attendions)

-1 is larger than 0
4294967295

Comment y remédier?

int main(){
    unsigned int a = 1;
    signed int b = -2;
    signed int c = a+b;
    if(c < 0)
        puts("-1 is smaller than 0");
        printf("%d\n", c);
}

La sortie du code ci-dessus:

-1 is smaller than 0
-1

Exemple 3)

int main(){
    unsigned short a = 1;
    signed short b = -2;
    if(a + b < 0)
        puts("-1 is smaller than 0");
        printf("%d\n", a+b);
}

Le dernier exemple a résolu le problème car a et b étaient tous deux convertis en int en raison de la promotion entière.

La sortie du code ci-dessus:

-1 is smaller than 0
-1

Si j'ai mélangé certains concepts, faites-le moi savoir. Merci ~

1
Lusha Li