Se eu tiver a seguinte declaração:
float a = 3.0 ;
isso é um erro? Li em um livro que 3.0
é um double
valor e que devo especificá-lo como float a = 3.0f
. É assim?
Se eu tiver a seguinte declaração:
float a = 3.0 ;
isso é um erro? Li em um livro que 3.0
é um double
valor e que devo especificá-lo como float a = 3.0f
. É assim?
;
depois.
Respostas:
Não é um erro declarar float a = 3.0
: se o fizer, o compilador converterá o duplo literal 3.0 em um float para você.
No entanto, você deve usar a notação de literais flutuantes em cenários específicos.
Por motivos de desempenho:
Especificamente, considere:
float foo(float x) { return x * 0.42; }
Aqui, o compilador emitirá uma conversão (que você pagará no tempo de execução) para cada valor retornado. Para evitá-lo, você deve declarar:
float foo(float x) { return x * 0.42f; } // OK, no conversion required
Para evitar bugs ao comparar os resultados:
por exemplo, a seguinte comparação falha:
float x = 4.2;
if (x == 4.2)
std::cout << "oops"; // Not executed!
Podemos corrigir isso com a notação literal float:
if (x == 4.2f)
std::cout << "ok !"; // Executed!
(Observação: é claro, não é assim que você deve comparar números flutuantes ou duplos para igualdade em geral )
Para chamar a função sobrecarregada correta (pelo mesmo motivo):
Exemplo:
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;
}
Conforme observado pela Cyber , em um contexto de dedução de tipo, é necessário ajudar o compilador a deduzir float
:
No caso de auto
:
auto d = 3; // int
auto e = 3.0; // double
auto f = 3.0f; // float
E da mesma forma, no caso de dedução do tipo de modelo:
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;
}
42
está um inteiro, que é automaticamente promovido para float
(e isso acontecerá no momento da compilação em qualquer compilador decente), portanto, não há penalidade de desempenho. Provavelmente você quis dizer algo assim 42.0
.
4.2
para 4.2f
pode ter o efeito colateral de definir a FE_INEXACT
bandeira, dependendo do compilador e do sistema, e alguns (reconhecidamente poucos) programas se preocupam com que as operações de ponto flutuante são exatas, e que não são, e teste para que a bandeira . Isso significa que a transformação simples e óbvia em tempo de compilação altera o comportamento do programa.
float foo(float x) { return x*42.0; }
pode ser compilado para uma multiplicação de precisão simples e foi compilado pelo Clang da última vez que tentei. No entanto, float foo(float x) { return x*0.1; }
não pode ser compilado para uma única multiplicação de precisão única. Pode ter sido um pouco otimista demais antes deste patch, mas depois do patch ele deve apenas combinar conversion-double_precision_op-conversion a single_precision_op quando o resultado é sempre o mesmo. article.gmane.org/gmane.comp.compilers.llvm.cvs/167800/match=
someFloat
, a expressão someFloat * 0.1
terá resultados mais precisos do que someFloat * 0.1f
, embora em muitos casos seja mais barata do que uma divisão de ponto flutuante. Por exemplo, (float) (167772208.0f * 0.1) será arredondado corretamente para 16777220 em vez de 16777222. Alguns compiladores podem substituir uma double
multiplicação por uma divisão de ponto flutuante, mas para aqueles que não o fazem (é seguro para muitos, mas não todos os valores ) a multiplicação pode ser uma otimização útil, mas apenas se realizada com um double
recíproco.
O compilador transformará qualquer um dos seguintes literais em flutuantes, porque você declarou a variável como flutuante.
float a = 3; // converted to float
float b = 3.0; // converted to float
float c = 3.0f; // float
Importaria se você usasse auto
(ou outro tipo de método de dedução), por exemplo:
auto d = 3; // int
auto e = 3.0; // double
auto f = 3.0f; // float
auto
não é o único caso.
Literais de ponto flutuante sem um sufixo são do tipo duplo , isso é abordado no rascunho da seção padrão C ++ 2.14.4
Literais flutuantes :
[...] O tipo de um literal flutuante é duplo, a menos que seja explicitamente especificado por um sufixo. [...]
então, é um erro atribuir 3.0
um literal duplo a um float ?:
float a = 3.0
Não, ele será convertido, o que é abordado na seção 4.8
Conversões de ponto flutuante :
Um prvalue do tipo de ponto flutuante pode ser convertido em um prvalue de outro tipo de ponto flutuante. Se o valor de origem pode ser representado exatamente no tipo de destino, o resultado da conversão é essa representação exata. Se o valor de origem estiver entre dois valores de destino adjacentes, o resultado da conversão é uma escolha definida pela implementação de qualquer um desses valores. Caso contrário, o comportamento é indefinido.
Podemos ler mais detalhes sobre as implicações disso em GotW # 67: double or nothing que diz:
Isso significa que uma constante dupla pode ser convertida implicitamente (ou seja, silenciosamente) em uma constante flutuante, mesmo que isso perca a precisão (ou seja, dados). Isso foi permitido por motivos de compatibilidade e usabilidade do C, mas vale a pena ter em mente quando você faz o trabalho de ponto flutuante.
Um compilador de qualidade irá avisá-lo se você tentar fazer algo que tenha um comportamento indefinido, ou seja, colocar uma quantidade dupla em um valor flutuante que seja menor que o mínimo ou maior que o valor máximo que um valor flutuante é capaz de representar. Um compilador realmente bom fornecerá um aviso opcional se você tentar fazer algo que pode ser definido, mas pode perder informações, ou seja, colocar uma quantidade dupla em um float que está entre os valores mínimo e máximo representáveis por um float, mas que não pode ser representado exatamente como um flutuador.
Portanto, há ressalvas para o caso geral das quais você deve estar ciente.
De uma perspectiva prática, neste caso, os resultados provavelmente serão os mesmos, embora tecnicamente haja uma conversão, podemos ver isso experimentando o seguinte código em 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;
}
e vemos que os resultados para func1
e func2
são idênticos, usando clang
e gcc
:
func1():
movss xmm0, DWORD PTR .LC0[rip]
ret
func2():
movss xmm0, DWORD PTR .LC0[rip]
ret
Como Pascal aponta neste comentário, você nem sempre poderá contar com isso. Usar 0.1
e, 0.1f
respectivamente, faz com que o assembly gerado seja diferente, pois a conversão agora deve ser feita explicitamente. O seguinte código:
float func1(float x )
{
return x*0.1; // a double literal
}
float func2(float x)
{
return x*0.1f ; // a float literal
}
resulta na seguinte montagem:
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
Independentemente de saber se você pode determinar se a conversão terá um impacto no desempenho ou não, o uso do tipo correto documenta melhor sua intenção. Usar uma conversão explícita, por exemplo, static_cast
também ajuda a esclarecer se a conversão foi intencional e não acidental, o que pode significar um bug ou um bug em potencial.
Nota
Como supercat aponta, multiplicação por eg 0.1
e 0.1f
não é equivalente. Vou apenas citar o comentário porque foi excelente e um resumo provavelmente não faria justiça:
Por exemplo, se f fosse igual a 100000224 (que é exatamente representável como um float), multiplicá-lo por um décimo deve render um resultado arredondado para 10000022, mas multiplicar por 0,1f produzirá, em vez disso, um resultado que arredonda erroneamente para 10000023 Se a intenção é dividir por dez, a multiplicação pela constante dupla 0,1 será provavelmente mais rápida do que a divisão por 10f e mais precisa do que a multiplicação por 0,1f.
Meu objetivo original era demonstrar um exemplo falso dado em outra pergunta, mas isso demonstra que problemas sutis podem existir em exemplos de brinquedos.
f = f * 0.1;
e f = f * 0.1f;
fazem coisas diferentes . Por exemplo, se f
fosse igual a 100000224 (que é exatamente representável como a float
), multiplicá-lo por um décimo deve produzir um resultado arredondado para 10000022, mas multiplicar por 0,1f produzirá, em vez disso, um resultado que arredondará erroneamente para 10000023. Se a intenção é dividir por dez, a multiplicação por double
0,1 constante provavelmente será mais rápida do que a divisão por 10f
e mais precisa do que a multiplicação por 0.1f
.
Não é um erro no sentido de que o compilador irá rejeitá-lo, mas é um erro no sentido de que pode não ser o que você deseja.
Como seu livro afirma corretamente, 3.0
é um valor do tipo double
. Há uma conversão implícita de double
para float
, então float a = 3.0;
é uma definição válida de uma variável.
No entanto, pelo menos conceitualmente, isso executa uma conversão desnecessária. Dependendo do compilador, a conversão pode ser realizada em tempo de compilação ou pode ser salva para tempo de execução. Um motivo válido para salvá-lo para o tempo de execução é que as conversões de ponto flutuante são difíceis e podem ter efeitos colaterais inesperados se o valor não puder ser representado exatamente e nem sempre é fácil verificar se o valor pode ser representado exatamente.
3.0f
evita esse problema: embora tecnicamente, o compilador ainda pode calcular a constante em tempo de execução (sempre é), aqui, não há absolutamente nenhuma razão para que qualquer compilador possa fazer isso.
Embora não seja um erro, por si só, é um pouco desleixado. Você sabe que quer um float, então inicialize-o com um float.
Outro programador pode aparecer e não ter certeza de qual parte da declaração está correta, o tipo ou o inicializador. Por que não fazer com que ambos estejam corretos?
float Answer = 42.0f;
Quando você define uma variável, ela é inicializada com o inicializador fornecido. Isso pode exigir a conversão do valor do inicializador para o tipo da variável que está sendo inicializada. Isso é o que acontece quando você diz float a = 3.0;
: o valor do inicializador é convertido para float
e o resultado da conversão se torna o valor inicial de a
.
Em geral, não há problema, mas não custa nada escrever 3.0f
para mostrar que você está ciente do que está fazendo e, principalmente, se deseja escrever auto a = 3.0f
.
Se você tentar o seguinte:
std::cout << sizeof(3.2f) <<":" << sizeof(3.2) << std::endl;
você obterá a saída como:
4:8
que mostra, o tamanho de 3.2f é considerado como 4 bytes na máquina de 32 bits, enquanto 3.2 é interpretado como valor duplo levando 8 bytes na máquina de 32 bits. Isso deve fornecer a resposta que você está procurando.
double
e float
são diferentes, não responde se você pode inicializar a a float
partir de um literal duplo
O compilador deduz o tipo mais adequado a partir dos literais, ou pelo menos o que ele acha que é o mais adequado. Isso é perder eficiência em vez de precisão, ou seja, usar um double em vez de float. Em caso de dúvida, use os inicializadores de chaves para torná-lo explícito:
auto d = double{3}; // make a double
auto f = float{3}; // make a float
auto i = int{3}; // make a int
A história fica mais interessante se você inicializar a partir de outra variável onde as regras de conversão de tipo se aplicam: Embora seja legal construir uma forma dupla de literal, não pode ser construído a partir de um int sem possível estreitamento:
auto xxx = double{i} // warning ! narrowing conversion of 'i' from 'int' to 'double'
3.0
em um float para você. O resultado final é indistinguível defloat a = 3.0f
.