tipos - ¿Por qué C++ promueve un int a un flotante cuando un flotante no puede representar todos los valores int?
variable float (9)
¿Puede un
float
representar todos los valoresint
?
Para un sistema moderno típico donde
int
y
float
se almacenan en 32 bits, no.
Alguien tiene que ceder.
Los enteros de 32 bits no se asignan de 1 a 1 en un conjunto del mismo tamaño que incluye fracciones.
El
i
será promovido afloat
y los dos números defloat
serán comparados ...
No necesariamente. Realmente no sabes qué precisión se aplicará. C ++ 14 §5 / 12:
Los valores de los operandos flotantes y los resultados de las expresiones flotantes pueden representarse con mayor precisión y rango que el requerido por el tipo; los tipos no se cambian de ese modo.
Aunque
i
después de la promoción tiene
float
tipo nominal, el valor puede representarse utilizando hardware
double
.
C ++ no garantiza la pérdida o desbordamiento de precisión de punto flotante.
(Esto no es nuevo en C ++ 14; se hereda de C desde tiempos antiguos).
¿Por qué no promover tanto el
int
como elfloat
a undouble
?
Si desea una precisión óptima en todas partes, use el
double
y nunca verá un
float
.
O
long double
, pero eso podría correr más lento.
Las reglas están diseñadas para ser relativamente sensatas para la mayoría de los casos de uso de tipos de precisión limitada, teniendo en cuenta que una máquina puede ofrecer varias precisiones alternativas.
La mayoría de las veces, rápido y suelto es lo suficientemente bueno, por lo que la máquina es libre de hacer lo que sea más fácil. Eso podría significar una comparación redondeada, de precisión simple, o doble precisión y sin redondeo.
Pero, tales reglas son en última instancia compromisos, y algunas veces fallan. Para especificar con precisión la aritmética en C ++ (o C), ayuda a hacer explícitas las conversiones y promociones. Muchas guías de estilo para software extra confiable prohíben el uso de conversiones implícitas por completo, y la mayoría de los compiladores ofrecen advertencias para ayudarlo a eliminarlas.
Para conocer cómo surgieron estos compromisos, puede leer detenidamente el documento de justificación C. (La última edición cubre hasta C99.) No se trata solo de equipaje sin sentido desde los días del PDP-11 o K&R.
Digamos que tengo lo siguiente:
int i = 23;
float f = 3.14;
if (i == f) // do something
seré promovido a
float
y se compararán los dos números
float
, pero ¿puede una
float
representar todos los valores
int
?
¿Por qué no promover tanto el
int
como el
float
a un
double
?
Cuando
int
se promociona a
unsigned
en las promociones integrales, los valores negativos también se pierden (lo que lleva a que diversión como
0u < -1
sea verdadera).
Como la mayoría de los mecanismos en C (que se heredan en C ++), las conversiones aritméticas habituales deben entenderse en términos de operaciones de hardware. Los creadores de C estaban muy familiarizados con el lenguaje ensamblador de las máquinas con las que trabajaban, y escribieron C para tener sentido inmediato para ellos y para las personas como ellos al escribir cosas que hasta entonces se habrían escrito en ensamblador (como UNIX núcleo).
Ahora, los procesadores, por regla general, no tienen instrucciones de tipo mixto (agregue float a double, compare int a float, etc.) porque sería un gran desperdicio de bienes raíces en la oblea: tendría que implementar tantas veces más códigos de operación como desee para admitir diferentes tipos. El hecho de que solo tenga instrucciones para "agregar int a int", "comparar flotante con flotante", "multiplicar sin signo con sin signo", etc., hace que las conversiones aritméticas habituales sean necesarias en primer lugar: son un mapeo de dos tipos a la instrucción familia que tiene más sentido usar con ellos.
Desde el punto de vista de alguien que está acostumbrado a escribir código de máquina de bajo nivel, si tiene tipos mixtos, las instrucciones de ensamblador que es más probable que considere en el caso general son aquellas que requieren la menor cantidad de conversiones.
Este es particularmente el caso con los puntos flotantes, donde las conversiones son costosas en tiempo de ejecución, y particularmente a principios de la década de 1970, cuando se desarrolló C, las computadoras eran lentas y cuando los cálculos de punto flotante se realizaban en software.
Esto se muestra en las conversiones aritméticas habituales: solo se convierte un operando (con la única excepción de
long
/
unsigned int
, donde
long
puede convertirse en
unsigned long
, lo que no requiere que se haga nada en la mayoría de las máquinas. Quizás no en cualquier lugar donde se aplique la excepción).
Por lo tanto, las conversiones aritméticas habituales se escriben para hacer lo que un codificador de ensamblaje haría la mayor parte del tiempo: tiene dos tipos que no se ajustan, convierta uno al otro para que así sea. Esto es lo que haría en el código de ensamblador a menos que tenga una razón específica para hacer lo contrario, y para las personas que están acostumbradas a escribir código de ensamblador y tienen una razón específica para forzar una conversión diferente, solicitando explícitamente que la conversión sea natural. Después de todo, simplemente puedes escribir
if((double) i < (double) f)
Es interesante notar en este contexto, por cierto, que
unsigned
es más alto en la jerarquía que
int
, por lo que comparar
int
con
unsigned
terminará en una comparación sin signo (de ahí el bit
0u < -1
desde el principio).
Sospecho que esto es un indicador de que las personas en los viejos tiempos consideraban menos
unsigned
menos como una restricción en
int
que como una extensión de su rango de valores: no necesitamos el signo en este momento, así que usemos el bit extra para un rango de valores mayor .
Lo usaría si tuviera razones para esperar que un
int
se desbordara, una preocupación mucho mayor en un mundo de
int
s de 16 bits.
Cuando se crea un lenguaje de programación, algunas decisiones se toman de forma intuitiva.
Por ejemplo, ¿por qué no convertir int + float en int + int en lugar de float + float o double + double? ¿Por qué llamar a int-> float una promoción si tiene los mismos bits? ¿Por qué no llamar a float-> int una promoción?
Si confía en conversiones de tipo implícito, debe saber cómo funcionan, de lo contrario, simplemente realice la conversión manualmente.
Algún idioma podría haber sido diseñado sin ninguna conversión de tipo automática. Y no todas las decisiones durante una fase de diseño podrían haberse tomado lógicamente con una buena razón.
JavaScript con su escritura de pato tiene decisiones aún más oscuras bajo el capó. Diseñar un lenguaje absolutamente lógico es imposible, creo que va al teorema de incompletitud de Godel. Tienes que equilibrar la lógica, la intuición, la práctica y los ideales.
Es fascinante que varias respuestas aquí discutan desde el origen del lenguaje C, nombrando explícitamente K&R y el bagaje histórico como la razón por la que un int se convierte en un flotador cuando se combina con un flotador.
Esto está señalando la culpa a las partes equivocadas. En K&R C, no existía el cálculo flotante. Todas las operaciones de coma flotante se realizaron con doble precisión. Por esa razón, un entero (o cualquier otra cosa) nunca se convirtió implícitamente en un flotante, sino solo en un doble. Un flotante tampoco podría ser el tipo de argumento de función: tenía que pasar un puntero para flotar si realmente, realmente, realmente quería evitar la conversión en un doble. Por esa razón, las funciones
int x(float a)
{ ... }
y
int y(a)
float a;
{ ... }
tener diferentes convenciones de llamadas. El primero obtiene un argumento flotante, el segundo (ya no se permite como sintaxis) obtiene un argumento doble.
Los argumentos aritméticos y de función de coma flotante de precisión simple solo se introdujeron con ANSI C. Kernighan / Ritchie es inocente.
Ahora, con las nuevas expresiones de flotador único disponibles (el flotador único anteriormente solo era un formato de almacenamiento), también tenía que haber nuevas conversiones de tipos. Lo que sea que el equipo ANSI C eligió aquí (y estaría perdido para una mejor elección) no es culpa de K&R.
Incluso el
double
puede no ser capaz de representar todos los valores
int
, dependiendo de cuántos bits contenga
int
.
¿Por qué no promover tanto el int como el flotador a un doble?
Probablemente porque es más costoso convertir ambos tipos al
double
que usar uno de los operandos, que ya es
float
, como
float
.
También introduciría reglas especiales para operadores de comparación incompatibles con reglas para operadores aritméticos.
Tampoco hay garantía de cómo se representarán los tipos de coma flotante, por lo que sería un tiro ciego suponer que convertir
int
a
double
(o incluso
long double
) para la comparación resolverá cualquier cosa.
La pregunta es por qué: porque es rápido, fácil de explicar, fácil de compilar, y todas estas eran razones muy importantes en el momento en que se desarrolló el lenguaje C.
Podría haber tenido una regla diferente: que para cada comparación de valores aritméticos, el resultado es el de comparar los valores numéricos reales. Eso sería algo trivial si una de las expresiones comparadas es una constante, una instrucción adicional al comparar int con signo y sin signo, y bastante difícil si compara long long y double y desea resultados correctos cuando long long no puede representarse como double. (0u <-1 sería falso, porque compararía los valores numéricos 0 y -1 sin considerar sus tipos).
En Swift, el problema se resuelve fácilmente al no permitir operaciones entre diferentes tipos.
Las reglas de promoción de tipos están diseñadas para ser simples y funcionar de manera predecible. Los tipos en C / C ++ están naturalmente "ordenados" por el rango de valores que pueden representar. Vea this para más detalles. Aunque los tipos de coma flotante no pueden representar todos los enteros representados por tipos integrales porque no pueden representar el mismo número de dígitos significativos, pueden representar un rango más amplio.
Para tener un comportamiento predecible, cuando se requieren promociones de tipo, los tipos numéricos siempre se convierten al tipo con el rango más grande para evitar el desbordamiento en el más pequeño. Imagina esto:
int i = 23464364; // more digits than float can represent!
float f = 123.4212E36f; // larger range than int can represent!
if (i == f) { /* do something */ }
Si la conversión se realizó hacia el tipo integral, el flotador
f
se desbordaría cuando se convirtiera en int, lo que conduciría a un comportamiento indefinido.
Por otro lado, convertir
i
a
f
solo causa una pérdida de precisión que es irrelevante ya que
f
tiene la misma precisión, por lo que aún es posible que la comparación tenga éxito.
Depende del programador en ese punto interpretar el resultado de la comparación de acuerdo con los requisitos de la aplicación.
Finalmente, además del hecho de que los números de coma flotante de doble precisión sufren el mismo problema que representa los enteros (número limitado de dígitos significativos), el uso de la promoción en ambos tipos conduciría a tener una representación de mayor precisión para
i
, mientras que
f
está condenado a tener el original precisión, por lo que la comparación no tendrá éxito si, para empezar, tengo dígitos más significativos que
f
.
Ahora, ese también es un comportamiento indefinido: la comparación podría tener éxito para algunas parejas (
i
,
f
) pero no para otras.
Las reglas están escritas para entradas de 16 bits (el tamaño más pequeño requerido). Su compilador con entradas de 32 bits seguramente convierte ambos lados al doble. De todos modos, no hay registros flotantes en el hardware moderno, por lo que debe convertirse a doble. Ahora, si tiene entradas de 64 bits, no estoy muy seguro de lo que hace. el doble largo sería apropiado (normalmente 80 bits pero ni siquiera es estándar).
Q1: ¿Puede un flotante representar todos los valores int?
IEE754 puede representar todos los enteros exactamente como flotantes, hasta aproximadamente 2 23 , como se menciona en esta answer .
P2: ¿Por qué no promover tanto el int como el float a un doble?
Las reglas en el Estándar para estas conversiones son ligeras modificaciones de las de K&R: las modificaciones acomodan los tipos agregados y las reglas de preservación del valor. Se agregó una licencia explícita para realizar cálculos en un tipo "más amplio" de lo absolutamente necesario, ya que esto a veces puede producir un código más pequeño y más rápido, sin mencionar la respuesta correcta con más frecuencia. Los cálculos también se pueden realizar en un tipo "más estrecho" por la regla como si siempre que se obtenga el mismo resultado final. La conversión explícita siempre se puede utilizar para obtener un valor en un tipo deseado.
Source
Realizar cálculos en un tipo más amplio significa que dado
float f1;
y
float f2;
,
f1 + f2
podría calcularse con
double
precisión.
Y significa que dado
int i;
y
float f;
,
i == f
podría calcularse con
double
precisión.
Pero no es necesario calcular
i == f
con doble precisión, como se indica en el comentario hvd.
También el estándar C lo dice. Estas se conocen como las conversiones aritméticas habituales. La siguiente descripción está tomada directamente del estándar ANSI C.
... si cualquiera de los operandos tiene tipo float, el otro operando se convierte a tipo float.
Source y también puedes verlo en la ref .
Un enlace relevante es esta answer . Una fuente más analítica está here .
Aquí hay otra forma de explicar esto: las conversiones aritméticas habituales se realizan implícitamente para convertir sus valores en un tipo común. El compilador primero realiza la promoción de enteros, si los operandos aún tienen tipos diferentes, entonces se convierten al tipo que aparece más arriba en la siguiente jerarquía: