Reglas de promoción de tipo implícito

Esta publicación está destinada a ser utilizada como una pregunta frecuente sobre la promoción de entero implícito en C, particularmente la promoción implícita causada por las conversiones aritméticas habituales y / o las promociones enteras.

Ejemplo 1)
¿Por qué esto da un número entero extraño y grande y no 255?

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

Ejemplo 2)
¿Por qué esto da “-1 es más grande que 0”?

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

Ejemplo 3)
¿Por qué cambiar el tipo en el ejemplo anterior para solucionar short el problema?

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

(Estos ejemplos estaban destinados a una computadora de 32 o 64 bits con capacidad de 16 bits).

C fue diseñado para cambiar de forma implícita y silenciosa los tipos enteros de los operandos utilizados en las expresiones. Existen varios casos en los que el lenguaje obliga al comstackdor a cambiar los operandos a un tipo más grande o cambiar su firma.

La razón detrás de esto es evitar desbordamientos accidentales durante la aritmética, pero también permitir que los operandos con firmados diferentes coexistan en la misma expresión.

Desafortunadamente, las reglas para la promoción de tipos implícitos causan mucho más daño que bien, hasta el punto en que podrían ser uno de los mayores defectos en el lenguaje C. Estas reglas a menudo ni siquiera son conocidas por el progtwigdor promedio de C y por lo tanto causan todo tipo de errores muy sutiles.

En general, se ven escenarios en los que el progtwigdor dice “simplemente envía a escribir x y funciona”, pero no saben por qué. O tales errores se manifiestan a sí mismos como fenómenos raros e intermitentes que golpean desde un código aparentemente simple y directo. La promoción implícita es particularmente problemática en el código al realizar manipulaciones de bits, ya que la mayoría de los operadores bit-wise en C vienen con un comportamiento poco definido cuando se les entrega un operando con signo.


Tipos enteros y rango de conversión

Los tipos enteros en C son char , short , int , long , long long y enum .
_Bool / bool también se trata como un tipo entero cuando se trata de tipos de promociones.

Todos los enteros tienen un rango de conversión especificado. C11 6.3.1.1, énfasis mío en las partes más importantes:

Cada tipo entero tiene un rango de conversión entero definido de la siguiente manera:
– No hay dos tipos enteros con signo que tengan el mismo rango, incluso si tienen la misma representación.
– El rango de un tipo entero con signo debe ser mayor que el rango de cualquier tipo entero con signo con menos precisión.
– El rango de long long int será mayor que el rango de long int , que será mayor que el rango de int , que será mayor que el rango de short int , que será mayor que el rango de signed char .
– El rango de cualquier tipo de entero sin signo debe ser igual al rango del tipo entero con signo correspondiente, si lo hay.
– El rango de cualquier tipo de entero estándar será mayor que el rango de cualquier tipo de entero extendido con el mismo ancho.
– El rango de char será igual al rango de char firmado y char sin signo.
– El rango de _Bool debe ser menor que el rango de todos los demás tipos de enteros estándar.
– El rango de cualquier tipo enumerado debe ser igual al rango del tipo entero compatible (ver 6.7.2.2).

Los tipos de stdint.h clasifican aquí, con el mismo rango que cualquier tipo al que corresponda en el sistema dado. Por ejemplo, int32_t tiene el mismo rango que int en un sistema de 32 bits.

Además, C11 6.3.1.1 especifica qué tipos se consideran tipos enteros pequeños (no un término formal):

Lo siguiente puede usarse en una expresión donde se pueda usar una int o unsigned int :

– Un objeto o expresión con un tipo entero (diferente de int o unsigned int ) cuyo rango de conversión entero es menor o igual que el rango de int y unsigned int .

Lo que este texto algo críptico significa en la práctica, es que _Bool , char y short (y también int8_t , uint8_t , etc.) son los “tipos enteros pequeños”. Estos se tratan de maneras especiales y sujetos a promoción implícita, como se explica a continuación.


Las promociones enteras

Siempre que se usa un tipo entero pequeño en una expresión, se convierte implícitamente en int que siempre está firmado. Esto se conoce como las promociones enteras o la regla de promoción entera .

Formalmente, la regla dice (C11 6.3.1.1):

Si un int puede representar todos los valores del tipo original (como está restringido por el ancho, para un campo de bit), el valor se convierte en un int ; de lo contrario, se convierte a unsigned int . Estas se llaman promociones enteras .

Este texto a menudo se entiende mal como: “todos los tipos enteros pequeños y con signo se convierten a int firmado y todos los tipos enteros pequeños sin signo se convierten a unsigned int”. Esto es incorrecto. La parte sin signo aquí solo significa que si tenemos, por ejemplo, un operando unsigned short , y int tiene el mismo tamaño que short en el sistema dado, entonces el operando unsigned short se convierte en unsigned int . Como en, nada de nota realmente sucede. Pero en caso de que short sea ​​un tipo más pequeño que int , siempre se convierte en int (firmado), independientemente de si el short está firmado o no .

La dura realidad causada por las promociones enteras significa que casi ninguna operación en C puede llevarse a cabo en tipos pequeños como char o short . Las operaciones siempre se llevan a cabo en tipos int o más grandes.

Esto puede sonar como una tontería, pero afortunadamente el comstackdor puede optimizar el código. Por ejemplo, una expresión que contenga dos operandos unsigned char obtendría los operandos promocionados a int y la operación llevada a cabo como int . Pero el comstackdor puede optimizar la expresión para que realmente se lleve a cabo como una operación de 8 bits, como era de esperar. Sin embargo, aquí viene el problema: el comstackdor no tiene permiso para optimizar el cambio implícito de firma firmado por la promoción entera. Porque no hay forma de que el comstackdor diga si el progtwigdor se basa deliberadamente en la promoción implícita, o si no es intencional.

Es por eso que el ejemplo 1 en la pregunta falla. Ambos operandos de charla sin signo se promueven a tipo int , la operación se lleva a cabo en tipo int , y el resultado de x - y es de tipo int . Lo que significa que obtenemos -1 lugar de 255 que podría haberse esperado. El comstackdor puede generar código máquina que ejecuta el código con instrucciones de 8 bits en lugar de int , pero puede que no optimice el cambio de firma. Lo que significa que terminamos con un resultado negativo, que a su vez da como resultado un número extraño cuando printf("%u es invocado. El ejemplo 1 podría ser arreglado al convertir uno o ambos operandos para escribir unsigned int .

Con la excepción de algunos casos especiales, como ++ y el sizeof operadores, las promociones enteras se aplican a casi todas las operaciones en C, sin importar si se usan operadores unarios, binarios (o ternarios).


Las conversiones aritméticas usuales

Siempre que se realice una operación binaria (una operación con 2 operandos) en C, ambos operandos del operador deben ser del mismo tipo. Por lo tanto, en caso de que los operandos sean de tipos diferentes, C impone una conversión implícita de un operando al tipo del otro operando. Las reglas de cómo se hace esto se denominan las conversiones artihméticas habituales (a veces informalmente se denomina “equilibrio”). Estos se especifican en C11 6.3.18:

(Piense en esta regla como una statement if-else if larga, anidada y podría ser más fácil de leer :))

6.3.1.8 Conversiones aritméticas habituales

Muchos operadores que esperan operandos de tipo aritmético causan conversiones y producen tipos de resultados de forma similar. El propósito es determinar un tipo real común para los operandos y el resultado. Para los operandos especificados, cada operando se convierte, sin cambio de tipo de dominio, a un tipo cuyo tipo real correspondiente es el tipo real común. A menos que se indique explícitamente lo contrario, el tipo real común es también el tipo real correspondiente del resultado, cuyo tipo de dominio es el dominio de tipo de los operandos si son los mismos, y complejo de lo contrario. Este patrón se denomina conversiones aritméticas habituales :

  • Primero, si el tipo real correspondiente de cualquiera de los dos operandos es long double , el otro operando se convierte, sin cambio de tipo de dominio, a un tipo cuyo tipo real correspondiente es long double .
  • De lo contrario, si el tipo real correspondiente de cualquiera de los dos operandos es el double , el otro operando se convierte, sin cambio de tipo de dominio, en un tipo cuyo tipo real correspondiente es double .
  • De lo contrario, si el tipo real correspondiente de cualquiera de los dos operandos es float , el otro operando se convierte, sin cambio de tipo de dominio, en un tipo cuyo tipo real correspondiente es flotante.
  • De lo contrario, las promociones enteras se realizan en ambos operandos. A continuación, se aplican las siguientes reglas a los operandos promocionados:

    • Si ambos operandos tienen el mismo tipo, entonces no se necesita más conversión.
    • De lo contrario, si ambos operandos tienen tipos enteros con signo o ambos tienen tipos enteros sin signo, el operando con el tipo de rango de conversión entero menor se convierte al tipo del operando con mayor rango.
    • De lo contrario, si el operando que tiene un tipo entero sin signo tiene un rango mayor o igual al rango del tipo del otro operando, entonces el operando con tipo entero con signo se convierte al tipo del operando con tipo entero sin signo.
    • De lo contrario, si el tipo del operando con tipo entero con signo puede representar todos los valores del tipo del operando con tipo entero sin signo, entonces el operando con tipo entero sin signo se convierte al tipo del operando con tipo entero con signo.
    • De lo contrario, ambos operandos se convierten al tipo de entero sin signo correspondiente al tipo de operando con tipo de entero con signo.

Aquí es notable que las conversiones aritméticas habituales se aplican tanto a las variables de coma flotante como a las variables enteras. En el caso de los enteros, también podemos observar que las promociones enteras se invocan desde las conversiones aritméticas habituales. Y después de eso, cuando ambos operandos tienen al menos el rango de int , los operadores se equilibran al mismo tipo, con la misma firma.

Esta es la razón por la cual a + b en el ejemplo 2 da un resultado extraño. Ambos operandos son enteros y tienen, al menos, rango int , por lo que las promociones enteras no se aplican. Los operandos no son del mismo tipo: a es unsigned int y b está signed int . Por lo tanto, el operador b se convierte temporalmente a tipo unsigned int . Durante esta conversión, pierde la información del signo y termina como un gran valor.

La razón por la cual cambiar el tipo a short en el ejemplo 3 soluciona el problema es porque el short es un tipo entero pequeño. Lo que significa que ambos operandos se promueven en entero para escribir int que está firmado. Después de la promoción de enteros, ambos operandos tienen el mismo tipo ( int ), no se necesita más conversión. Y luego la operación se puede llevar a cabo en un tipo firmado como se esperaba.

De acuerdo con la publicación anterior, quiero dar más información sobre cada ejemplo.

Ejemplo 1)

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

Como el carácter sin signo es menor que int, aplicamos la promoción de entero sobre ellos, luego tenemos (int) x- (int) y = (int) (- 1) y unsigned int (-1) = 4294967295.

El resultado del código anterior: (igual que lo que esperábamos)

 4294967295 -1 

¿Como arreglarlo?

Intenté lo recomendado en la publicación anterior, pero en realidad no funciona. Aquí está el código basado en la publicación anterior:

cambiar uno de ellos a unsigned int

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

Como x ya es un entero sin signo, solo aplicamos la promoción de entero a y. Entonces obtenemos (unsigned int) x- (int) y. Como todavía no tienen el mismo tipo, aplicamos las conversiones aritméticas habituales, obtenemos (unsigned int) x- (unsigned int) y = 4294967295.

El resultado del código anterior: (igual que lo que esperábamos):

 4294967295 -1 

Del mismo modo, el siguiente código obtiene el mismo resultado:

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

cambiar ambos a unsigned int

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

Como ambos no tienen firma int, no se necesita una promoción de enteros. Por la conversión aritmética habitual (tienen el mismo tipo), (sin signo int) x- (sin signo int) y = 4294967295.

El resultado del código anterior: (igual que lo que esperábamos):

 4294967295 -1 

Una de las formas posibles de corregir el código: (agregue un tipo de molde al final)

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

El resultado del código anterior:

 4294967295 -1 255 

Ejemplo 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); } 

Como ambos son enteros, no se necesita una promoción entera. Con la conversión aritmética usual, obtenemos (unsigned int) a + (unsigned int) b = 1 + 4294967294 = 4294967295.

El resultado del código anterior: (igual que lo que esperábamos)

 -1 is larger than 0 4294967295 

¿Como arreglarlo?

 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); } 

El resultado del código anterior:

 -1 is smaller than 0 -1 

Ejemplo 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); } 

El último ejemplo solucionó el problema, ya que a y b se convirtieron en int debido a la promoción entera.

El resultado del código anterior:

 -1 is smaller than 0 -1 

Si tengo algunos conceptos confusos, házmelo saber. Gracias ~