Es 'flotar a = 3.0;' una afirmación correcta?

86

Si tengo la siguiente declaración:

float a = 3.0 ;

¿Eso es un error? Leí en un libro que 3.0es un doublevalor y que tengo que especificarlo float a = 3.0f. ¿Es tan?

TESLA____
fuente
2
El compilador convertirá el doble literal 3.0en un flotante por usted. El resultado final es indistinguible de float a = 3.0f.
David Heffernan
6
@EdHeal: Lo es, pero no es particularmente relevante para esta pregunta, que trata sobre las reglas de C ++.
Keith Thompson
20
Bueno, al menos necesitas un ;después.
Hot Licks
3
10 votos negativos y poco en los comentarios para explicarlos, muy desalentador. Esta es la primera pregunta de los OP y si la gente cree que vale 10 votos negativos, debería haber algunas explicaciones. Esta es una pregunta válida con implicaciones no obvias y muchas cosas interesantes que aprender de las respuestas y comentarios.
Shafik Yaghmour
3
@HotLicks no se trata de sentirse mal o bien, seguro que puede parecer injusto pero así es la vida, son puntos de unicornio al fin y al cabo. Los votos negativos seguramente no son para cancelar los votos a favor que no te gustan, al igual que los votos a favor no son para cancelar los votos en contra que no te gustan. Si la gente siente que la pregunta se puede mejorar, seguramente quien lo pregunte por primera vez debería recibir comentarios. No veo ninguna razón para votar en contra, pero me gustaría saber por qué otros lo hacen, aunque son libres de no decirlo.
Shafik Yaghmour

Respuestas:

159

No es un error declararlo float a = 3.0: si lo hace, el compilador convertirá el doble literal 3.0 en un flotante por usted.


Sin embargo, debe usar la notación de literales flotantes en escenarios específicos.

  1. Por motivos de rendimiento:

    Específicamente, considere:

    float foo(float x) { return x * 0.42; }
    

    Aquí el compilador emitirá una conversión (que pagará en tiempo de ejecución) por cada valor devuelto. Para evitarlo debes declarar:

    float foo(float x) { return x * 0.42f; } // OK, no conversion required
    
  2. Para evitar errores al comparar resultados:

    por ejemplo, la siguiente comparación falla:

    float x = 4.2;
    if (x == 4.2)
       std::cout << "oops"; // Not executed!
    

    Podemos arreglarlo con la notación literal flotante:

    if (x == 4.2f)
       std::cout << "ok !"; // Executed!
    

    (Nota: por supuesto, así no es como debe comparar números flotantes o dobles para la igualdad en general )

  3. Para llamar a la función sobrecargada correcta (por la misma razón):

    Ejemplo:

    void foo(float f) { std::cout << "\nfloat"; }
    
    void foo(double d) { std::cout << "\ndouble"; }
    
    int main()
    {       
        foo(42.0);   // calls double overload
        foo(42.0f);  // calls float overload
        return 0;
    }
    
  4. Como señaló Cyber , en un contexto de deducción de tipo, es necesario ayudar al compilador a deducir float:

    En caso de auto:

    auto d = 3;      // int
    auto e = 3.0;    // double
    auto f = 3.0f;   // float
    

    Y de manera similar, en caso de deducción por tipo de plantilla:

    void foo(float f) { std::cout << "\nfloat"; }
    
    void foo(double d) { std::cout << "\ndouble"; }
    
    template<typename T>
    void bar(T t)
    {
          foo(t);
    }
    
    int main()
    {   
        bar(42.0);   // Deduce double
        bar(42.0f);  // Deduce float
    
        return 0;
    }
    

Demo en vivo

quantdev
fuente
2
En el punto 1 42hay un número entero, que se promueve automáticamente a float(y sucederá en tiempo de compilación en cualquier compilador decente), por lo que no hay ninguna penalización de rendimiento. Probablemente quisiste decir algo como 42.0.
Matteo Italia
@MatteoItalia, sí, quise decir 42.0 ofc (editado, gracias)
quantdev
2
@ChristianHackl La conversión 4.2a 4.2fpuede tener el efecto secundario de establecer el FE_INEXACTindicador, según el compilador y el sistema, y ​​algunos (ciertamente pocos) programas se preocupan por qué operaciones de punto flotante son exactas y cuáles no, y prueban ese indicador . Esto significa que la simple transformación obvia en tiempo de compilación altera el comportamiento del programa.
6
float foo(float x) { return x*42.0; }se puede compilar en una multiplicación de precisión simple, y fue compilado por Clang la última vez que lo intenté. Sin embargo float foo(float x) { return x*0.1; }, no se puede compilar en una sola multiplicación de precisión simple. Puede haber sido un poco demasiado optimista antes de este parche, pero después del parche solo debería combinar conversion-double_precision_op-conversion con single_precision_op cuando el resultado sea siempre el mismo. article.gmane.org/gmane.comp.compilers.llvm.cvs/167800/match=
Pascal Cuoq
1
Si uno desea calcular un valor que es una décima parte de someFloat, la expresión someFloat * 0.1dará resultados más precisos que someFloat * 0.1f, aunque en muchos casos es más barata que una división de punto flotante. Por ejemplo, (float) (167772208.0f * 0.1) redondeará correctamente a 16777220 en lugar de 16777222. Algunos compiladores pueden sustituir una doublemultiplicación por una división de coma flotante, pero para aquellos que no lo hacen (es seguro para muchos, aunque no para todos los valores ) la multiplicación puede ser una optimización útil, pero solo si se realiza con un doublerecíproco.
supercat
22

El compilador convertirá cualquiera de los siguientes literales en flotantes, porque usted declaró la variable como flotante.

float a = 3;     // converted to float
float b = 3.0;   // converted to float
float c = 3.0f;  // float

Sería importante si utilizó auto(u otro tipo de métodos de deducción), por ejemplo:

auto d = 3;      // int
auto e = 3.0;    // double
auto f = 3.0f;   // float
Cory Kramer
fuente
5
Los tipos también se deducen cuando se utilizan plantillas, por autolo que no es el único caso.
Shafik Yaghmour
14

Los literales de coma flotante sin sufijo son de tipo double , esto se trata en la sección del estándar de C ++ del borrador 2.14.4 Literales flotantes :

[...] El tipo de un literal flotante es doble a menos que se especifique explícitamente con un sufijo. [...]

entonces, ¿es un error asignar 3.0un doble literal a un flotante ?:

float a = 3.0

No, no lo es, se convertirá, lo cual se trata en la sección 4.8 Conversiones de punto flotante :

Un prvalue de tipo de coma flotante se puede convertir en un prvalue de otro tipo de coma flotante. Si el valor de origen se puede representar exactamente en el tipo de destino, el resultado de la conversión es esa representación exacta. Si el valor de origen está entre dos valores de destino adyacentes, el resultado de la conversión es una elección definida por la implementación de cualquiera de esos valores. De lo contrario, el comportamiento no está definido.

Podemos leer más detalles sobre las implicaciones de esto en GotW # 67: doble o nada que dice:

Esto significa que una constante doble se puede convertir implícitamente (es decir, en silencio) en una constante flotante, incluso si al hacerlo se pierde precisión (es decir, datos). Se permitió que se mantuviera por razones de compatibilidad y usabilidad de C, pero vale la pena tenerlo en cuenta cuando se trabaja en punto flotante.

Un compilador de calidad le advertirá si intenta hacer algo que tiene un comportamiento indefinido, es decir, poner una cantidad doble en un valor flotante que sea menor que el valor mínimo, o mayor que el máximo, que un valor flotante es capaz de representar. Un compilador realmente bueno proporcionará una advertencia opcional si intenta hacer algo que puede estar definido pero podría perder información, es decir, poner una cantidad doble en un flotador que esté entre los valores mínimo y máximo representables por un flotador, pero que no puede representarse exactamente como un flotador.

Por lo tanto, hay advertencias para el caso general que debe tener en cuenta.

Desde una perspectiva práctica, en este caso, los resultados probablemente serán los mismos, aunque técnicamente hay una conversión, podemos ver esto probando el siguiente código en godbolt :

#include <iostream>

float func1()
{
  return 3.0; // a double literal
}


float func2()
{
  return 3.0f ; // a float literal
}

int main()
{  
  std::cout << func1() << ":" << func2() << std::endl ;
  return 0;
}

y vemos que los resultados para func1y func2son idénticos, usando tanto clangy gcc:

func1():
    movss   xmm0, DWORD PTR .LC0[rip]
    ret
func2():
    movss   xmm0, DWORD PTR .LC0[rip]
    ret

Como señala Pascal en este comentario , no siempre podrás contar con esto. El uso de 0.1y 0.1frespectivamente hace que el ensamblado generado sea diferente, ya que la conversión ahora debe realizarse explícitamente. El siguiente código:

float func1(float x )
{
  return x*0.1; // a double literal
}

float func2(float x)
{
  return x*0.1f ; // a float literal
}

da como resultado el siguiente montaje:

func1(float):  
    cvtss2sd    %xmm0, %xmm0    # x, D.31147    
    mulsd   .LC0(%rip), %xmm0   #, D.31147
    cvtsd2ss    %xmm0, %xmm0    # D.31147, D.31148
    ret
func2(float):
    mulss   .LC2(%rip), %xmm0   #, D.31155
    ret

Independientemente de si puede determinar si la conversión tendrá un impacto en el rendimiento o no, utilizar el tipo correcto documenta mejor su intención. El uso de conversiones explícitas, por ejemplo, static_casttambién ayuda a aclarar que la conversión fue intencionada en lugar de accidental, lo que puede significar un error o un error potencial.

Nota

Como señala supercat, la multiplicación por, por ejemplo, 0.1y 0.1fno es equivalente. Solo voy a citar el comentario porque fue excelente y un resumen probablemente no le haría justicia:

Por ejemplo, si f era igual a 100000224 (que es exactamente representable como un flotador), multiplicarlo por una décima debería producir un resultado que se redondea hacia abajo a 10000022, pero multiplicar por 0,1 f en su lugar producirá un resultado que erróneamente se redondea a 10000023 Si la intención es dividir por diez, la multiplicación por doble constante 0.1 probablemente será más rápida que la división por 10f y más precisa que la multiplicación por 0.1f.

Mi punto original era demostrar un ejemplo falso dado en otra pregunta, pero esto demuestra finamente que pueden existir problemas sutiles en los ejemplos de juguetes.

Shafik Yaghmour
fuente
1
Vale la pena señalar que las expresiones f = f * 0.1;y f = f * 0.1f; hacen cosas diferentes . Por ejemplo, si fera igual a 100000224 (que es exactamente representable como a float), multiplicarlo por una décima debería arrojar un resultado que se redondea a la baja a 10000022, pero multiplicar por 0.1f en su lugar producirá un resultado que erróneamente se redondea a 10000023. Si la intención es dividir por diez, la multiplicación por la doubleconstante 0.1 probablemente será más rápida que la división por 10fy más precisa que la multiplicación por 0.1f.
supercat
@supercat gracias por el buen ejemplo, te cité directamente, no dudes en editarlo como mejor te parezca.
Shafik Yaghmour
4

No es un error en el sentido de que el compilador lo rechazará, pero es un error en el sentido de que puede que no sea lo que desea.

Como dice correctamente su libro, 3.0es un valor de tipo double. Hay una conversión implícita de doublea float, por lo que float a = 3.0;es una definición válida de una variable.

Sin embargo, al menos conceptualmente, esto realiza una conversión innecesaria. Dependiendo del compilador, la conversión se puede realizar en tiempo de compilación o se puede guardar para tiempo de ejecución. Una razón válida para guardarlo para el tiempo de ejecución es que las conversiones de punto flotante son difíciles y pueden tener efectos secundarios inesperados si el valor no se puede representar exactamente, y no siempre es fácil verificar si el valor se puede representar exactamente.

3.0f evita ese problema: aunque técnicamente, el compilador todavía puede calcular la constante en tiempo de ejecución (siempre lo es), aquí, no hay absolutamente ninguna razón por la que cualquier compilador pueda hacer eso.


fuente
De hecho, en el caso de un compilador cruzado, sería bastante incorrecto que la conversión se realizara en el momento de la compilación, ya que estaría sucediendo en la plataforma incorrecta.
Marqués de Lorne
2

Aunque no es un error, en sí mismo, es un poco descuidado. Sabes que quieres un flotador, así que inicialízalo con un flotador.
Puede aparecer otro programador y no estar seguro de qué parte de la declaración es correcta, el tipo o el inicializador. ¿Por qué no hacer que ambos sean correctos?
respuesta flotante = 42.0f;

Ingeniero
fuente
0

Cuando define una variable, se inicializa con el inicializador proporcionado. Esto puede requerir convertir el valor del inicializador al tipo de variable que se está inicializando. Eso es lo que sucede cuando dice float a = 3.0;: el valor del inicializador se convierte en floaty el resultado de la conversión se convierte en el valor inicial dea .

En general, eso está bien, pero no está de más escribir 3.0fpara demostrar que eres consciente de lo que estás haciendo, y especialmente si quieres escribir auto a = 3.0f.

Kerrek SB
fuente
0

Si prueba lo siguiente:

std::cout << sizeof(3.2f) <<":" << sizeof(3.2) << std::endl;

obtendrá una salida como:

4:8

que muestra, el tamaño de 3.2f se toma como 4 bytes en una máquina de 32 bits, mientras que 3.2 se interpreta como un valor doble que toma 8 bytes en una máquina de 32 bits. Esto debería proporcionar la respuesta que está buscando.

Dr. Debasish Jana
fuente
Eso muestra eso doubley floatson diferentes, no responde si puede inicializar a floatdesde un doble literal
Jonathan Wakely
por supuesto, puede inicializar un flotante a partir de un valor doble sujeto a truncamiento de datos, si corresponde
Dr. Debasish Jana
4
Sí, lo sé, pero esa era la pregunta del OP, por lo que su respuesta no la respondió realmente, ¡a pesar de afirmar que proporciona la respuesta!
Jonathan Wakely
0

El compilador deduce el tipo que mejor se ajusta a los literales, o al menos lo que cree que se ajusta mejor. Eso es más bien perder eficiencia sobre precisión, es decir, utilizar un doble en lugar de un flotador. En caso de duda, utilice brace-inicializadores para hacerlo explícito:

auto d = double{3}; // make a double
auto f = float{3}; // make a float
auto i = int{3}; // make a int

La historia se vuelve más interesante si inicializa desde otra variable donde se aplican las reglas de conversión de tipo: si bien es legal construir una forma doble de un literal, no se puede construir a partir de un int sin un posible estrechamiento:

auto xxx = double{i} // warning ! narrowing conversion of 'i' from 'int' to 'double' 
trusquival
fuente