È "float a = 3.0;" una dichiarazione corretta?


86

Se ho la seguente dichiarazione:

float a = 3.0 ;

è un errore? Ho letto in un libro che 3.0è un doublevalore e che devo specificarlo come float a = 3.0f. È così?


2
Il compilatore convertirà il doppio letterale 3.0in un float per te. Il risultato finale è indistinguibile da float a = 3.0f.
David Heffernan

6
@EdHeal: lo è, ma non è particolarmente rilevante per questa domanda, che riguarda le regole C ++.
Keith Thompson

20
Beh, almeno hai bisogno di un ;dopo.
Hot Licks

3
10 voti negativi e non molto nei commenti per spiegarli, molto scoraggiante. Questa è la prima domanda del PO e se le persone ritengono che valga 10 voti negativi, dovrebbero esserci alcune spiegazioni. Questa è una domanda valida con implicazioni non ovvie e molte cose interessanti da imparare dalle risposte e dai commenti.
Shafik Yaghmour

3
@HotLicks non si tratta di sentirsi male o bene, certo può sembrare ingiusto ma questa è la vita, dopotutto sono punti di unicorno. I voti favorevoli sicuramente non servono a cancellare i voti positivi che non ti piacciono, proprio come i voti positivi non servono a cancellare i voti negativi che non ti piacciono. Se le persone ritengono che la domanda possa essere migliorata, sicuramente chi chiede per la prima volta dovrebbe ricevere un feedback. Non vedo alcun motivo per sottoporre a downvote, ma vorrei sapere perché gli altri lo fanno anche se sono liberi di non dirlo.
Shafik Yaghmour

Risposte:


159

Non è un errore da dichiarare float a = 3.0: se lo fai, il compilatore convertirà il doppio letterale 3.0 in un float per te.


Tuttavia, dovresti usare la notazione dei letterali float in scenari specifici.

  1. Per motivi di prestazioni:

    In particolare, considera:

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

    Qui il compilatore emetterà una conversione (che pagherai in fase di esecuzione) per ogni valore restituito. Per evitarlo dovresti dichiarare:

    float foo(float x) { return x * 0.42f; } // OK, no conversion required
    
  2. Per evitare bug durante il confronto dei risultati:

    es. il seguente confronto fallisce:

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

    Possiamo risolverlo con la notazione letterale float:

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

    (Nota: ovviamente, non è così che dovresti confrontare i numeri in virgola mobile o doppi per l'uguaglianza in generale )

  3. Per chiamare la funzione di sovraccarico corretta (per lo stesso motivo):

    Esempio:

    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. Come notato da Cyber , in un contesto di deduzione del tipo, è necessario aiutare il compilatore a dedurre float:

    In caso di auto:

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

    Allo stesso modo, in caso di detrazione del tipo di modello:

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

Dimostrazione dal vivo


2
Al punto 1 42c'è un numero intero, che viene automaticamente promosso a float(e accadrà in fase di compilazione in qualsiasi compilatore decente), quindi non c'è penalità nelle prestazioni. Probabilmente intendevi qualcosa di simile 42.0.
Matteo Italia

@MatteoItalia, sì, intendevo 42.0 ofc (modificato, grazie)
quantdev

2
@ChristianHackl La conversione 4.2in 4.2fpuò avere l'effetto collaterale di impostare il FE_INEXACTflag, a seconda del compilatore e del sistema, e alcuni (certamente pochi) programmi si preoccupano di quali operazioni a virgola mobile sono esatte e quali no, e testano quel flag . Ciò significa che la semplice e ovvia trasformazione in fase di compilazione altera il comportamento del programma.

6
float foo(float x) { return x*42.0; }può essere compilato con una moltiplicazione a precisione singola, ed è stato compilato così da Clang l'ultima volta che ho provato. Tuttavia float foo(float x) { return x*0.1; }non può essere compilato su una singola moltiplicazione a precisione singola. Potrebbe essere stato un po 'troppo ottimistico prima di questa patch, ma dopo la patch dovrebbe solo combinare conversion-double_precision_op-conversion a single_precision_op quando il risultato è sempre lo stesso. article.gmane.org/gmane.comp.compilers.llvm.cvs/167800/match=
Pascal Cuoq

1
Se si desidera calcolare un valore che è un decimo di someFloat, l'espressione someFloat * 0.1darà risultati più accurati rispetto a someFloat * 0.1f, mentre in molti casi sarà più economica di una divisione in virgola mobile. Ad esempio, (float) (167772208.0f * 0.1) verrà arrotondato correttamente a 16777220 anziché a 16777222. Alcuni compilatori possono sostituire una doublemoltiplicazione per una divisione in virgola mobile, ma per quelli che non lo fanno (è sicuro per molti anche se non tutti i valori ) la moltiplicazione può essere un'ottimizzazione utile, ma solo se eseguita con un doublereciproco.
supercat

22

Il compilatore trasformerà uno qualsiasi dei seguenti valori letterali in float, perché hai dichiarato la variabile come float.

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

Importerebbe se usassi auto(o altri metodi di detrazione del tipo), ad esempio:

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

5
I tipi vengono anche dedotti quando si utilizzano modelli, quindi autonon è l'unico caso.
Shafik Yaghmour

14

Galleggianti letterali punto senza suffisso sono di tipo doppio , questo è trattato nel progetto C ++ parte standard 2.14.4 Floating letterali :

[...] Il tipo di un letterale floating è double a meno che non sia specificato esplicitamente da un suffisso. [...]

quindi è un errore assegnare 3.0un doppio letterale a un float ?:

float a = 3.0

No, non lo è, verrà convertito, come descritto nella sezione 4.8 Conversioni in virgola mobile :

Un prvalue di tipo a virgola mobile può essere convertito in un prvalue di un altro tipo di virgola mobile. Se il valore di origine può essere rappresentato esattamente nel tipo di destinazione, il risultato della conversione è quella rappresentazione esatta. Se il valore di origine è compreso tra due valori di destinazione adiacenti, il risultato della conversione è una scelta definita dall'implementazione di uno di questi valori. In caso contrario, il comportamento non è definito.

Possiamo leggere maggiori dettagli sulle implicazioni di questo in GotW # 67: double or nothing che dice:

Ciò significa che una doppia costante può essere convertita implicitamente (cioè silenziosamente) in una costante float, anche se così facendo si perde precisione (cioè dati). Questo è stato consentito per motivi di compatibilità e usabilità C, ma vale la pena tenerlo a mente quando si lavora in virgola mobile.

Un compilatore di qualità ti avviserà se provi a fare qualcosa che ha un comportamento indefinito, vale a dire mettere una quantità doppia in un float che è inferiore al valore minimo, o maggiore del massimo, che un float è in grado di rappresentare. Un buon compilatore fornirà un avviso opzionale se provi a fare qualcosa che può essere definito ma potrebbe perdere informazioni, ovvero mettere una quantità doppia in un float che è tra i valori minimo e massimo rappresentabili da un float, ma che non può essere rappresentato esattamente come un galleggiante.

Quindi ci sono avvertenze per il caso generale di cui dovresti essere a conoscenza.

Da un punto di vista pratico, in questo caso i risultati saranno molto probabilmente gli stessi anche se tecnicamente c'è una conversione, possiamo vederlo provando il seguente codice su 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 vediamo che i risultati per func1e func2sono identici, utilizzando sia clange gcc:

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

Come fa notare Pascal in questo commento non potrai sempre contare su questo. L'uso di 0.1e 0.1frispettivamente fa sì che l'assembly generato differisca poiché la conversione deve ora essere eseguita in modo esplicito. Il codice seguente:

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

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

risulta nel seguente assemblaggio:

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

Indipendentemente dal fatto che tu possa determinare se la conversione avrà o meno un impatto sulle prestazioni, l'utilizzo del tipo corretto documenta meglio la tua intenzione. Ad esempio, utilizzando conversioni esplicitestatic_cast aiuta anche a chiarire che la conversione era intesa e non accidentale, il che potrebbe significare un bug o un potenziale bug.

Nota

Come sottolinea supercat, la moltiplicazione per es 0.1e 0.1fnon è equivalente. Citerò solo il commento perché è stato eccellente e un riassunto probabilmente non gli renderebbe giustizia:

Ad esempio, se f fosse uguale a 100000224 (che è esattamente rappresentabile come float), moltiplicandolo per un decimo dovrebbe produrre un risultato che arrotonda per difetto a 10000022, ma moltiplicando per 0,1f produrrà invece un risultato che arrotonda erroneamente fino a 10000023 Se l'intenzione è di dividere per dieci, la moltiplicazione per doppia costante 0,1 sarà probabilmente più veloce della divisione per 10f e più precisa della moltiplicazione per 0,1f.

Il mio punto originale era quello di dimostrare un falso esempio fornito in un'altra domanda, ma questo dimostra con precisione che possono esistere problemi sottili negli esempi di giocattoli.


1
Può valere la pena notare che le espressioni f = f * 0.1;e f = f * 0.1f; fanno cose diverse . Ad esempio, se ffosse uguale a 100000224 (che è esattamente rappresentabile come a float), moltiplicandolo per un decimo dovrebbe produrre un risultato che arrotonda per difetto a 10000022, ma moltiplicando per 0,1f produrrà invece un risultato che arrotonda erroneamente a 10000023. Se l'intenzione è di dividere per dieci, la moltiplicazione per la doublecostante 0.1 sarà probabilmente più veloce della divisione per 10fe più precisa della moltiplicazione per 0.1f.
supercat

@supercat grazie per il bell'esempio, ti ho citato direttamente, sentiti libero di modificare come meglio credi.
Shafik Yaghmour

4

Non è un errore nel senso che il compilatore lo rifiuterà, ma è un errore nel senso che potrebbe non essere quello che vuoi.

Come afferma correttamente il tuo libro, 3.0è un valore di tipo double. C'è una conversione implicita da doublea float, quindi float a = 3.0;è una definizione valida di una variabile.

Tuttavia, almeno concettualmente, questo esegue una conversione inutile. A seconda del compilatore, la conversione può essere eseguita in fase di compilazione o può essere salvata per il runtime. Un motivo valido per salvarlo per il tempo di esecuzione è che le conversioni in virgola mobile sono difficili e possono avere effetti collaterali imprevisti se il valore non può essere rappresentato esattamente e non è sempre facile verificare se il valore può essere rappresentato esattamente.

3.0f evita questo problema: sebbene tecnicamente, il compilatore è ancora autorizzato a calcolare la costante in fase di esecuzione (lo è sempre), qui, non c'è assolutamente alcun motivo per cui un compilatore potrebbe farlo.


Infatti, nel caso di un cross-compilatore sarebbe del tutto errato che la conversione venga eseguita in fase di compilazione, perché avverrebbe sulla piattaforma sbagliata.
Marchese di Lorne

2

Sebbene non sia un errore, di per sé, è un po 'sciatto. Sai che vuoi un float, quindi inizializzalo con un float.
Un altro programmatore potrebbe arrivare e non essere sicuro di quale parte della dichiarazione sia corretta, il tipo o l'inizializzatore. Perché non fare in modo che entrambi siano corretti?
risposta a virgola mobile = 42.0f;


0

Quando si definisce una variabile, viene inizializzata con l'inizializzatore fornito. Ciò potrebbe richiedere la conversione del valore dell'inizializzatore nel tipo della variabile che viene inizializzata. Questo è ciò che accade quando dici float a = 3.0;: il valore dell'inizializzatore viene convertito in floate il risultato della conversione diventa il valore iniziale di a.

In genere va bene, ma non fa male scrivere 3.0fper dimostrare che sei consapevole di quello che stai facendo, soprattutto se vuoi scrivere auto a = 3.0f.


0

Se provi quanto segue:

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

otterrai l'output come:

4:8

ciò mostra, la dimensione di 3.2f viene presa come 4 byte su una macchina a 32 bit, mentre 3.2 viene interpretata come un valore doppio che prende 8 byte su una macchina a 32 bit. Questo dovrebbe fornire la risposta che stai cercando.


Ciò dimostra che doublee floatsono diversi, non risponde se è possibile inizializzare un floatda un doppio letterale
Jonathan Wakely

Ovviamente puoi inizializzare un float da un valore doppio soggetto al troncamento dei dati, se applicabile
Dr. Debasish Jana

4
Sì, lo so, ma quella era la domanda dell'OP, quindi la tua risposta non è riuscita a rispondere effettivamente, nonostante abbia affermato di fornire la risposta!
Jonathan Wakely

0

Il compilatore deduce il tipo più adatto dai letterali, o almeno ciò che ritiene sia il più adatto. Questo è piuttosto perdere efficienza rispetto alla precisione, cioè utilizzare un double invece di float. In caso di dubbio, utilizzare le parentesi graffe per renderlo esplicito:

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

La storia diventa più interessante se si inizializza da un'altra variabile in cui si applicano le regole di conversione del tipo: sebbene sia legale costruire una forma doppia in letterale, non può essere costruita da un int senza possibile restringimento:

auto xxx = double{i} // warning ! narrowing conversion of 'i' from 'int' to 'double' 
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.