Czy „float a = 3.0;” prawidłowe stwierdzenie?


86

Jeśli mam następującą deklarację:

float a = 3.0 ;

czy to błąd? Przeczytałem w książce, która 3.0jest doublewartością i muszę ją określić jako float a = 3.0f. Czy tak jest?


2
Kompilator przekształci podwójny literał 3.0w zmiennoprzecinkowy. Wynik końcowy jest nie do odróżnienia od float a = 3.0f.
David Heffernan

6
@EdHeal: Tak, ale nie jest to szczególnie istotne w przypadku tego pytania, które dotyczy reguł C ++.
Keith Thompson

20
Cóż, przynajmniej potrzebujesz ;później.
Hot Licks

3
10 głosów przeciw i niewiele w komentarzach, aby je wyjaśnić, bardzo zniechęcające. To jest pierwsze pytanie dotyczące PO i jeśli ludzie uważają, że jest to warte 10 głosów przeciw, powinno być kilka wyjaśnień. To ważne pytanie z nieoczywistymi implikacjami i wieloma interesującymi rzeczami, których można się nauczyć z odpowiedzi i komentarzy.
Shafik Yaghmour

3
@HotLicks nie chodzi o to, aby czuć się źle lub dobrze, na pewno może się to wydawać niesprawiedliwe, ale takie jest życie, w końcu to punkty jednorożca. Dowvoty z pewnością nie mają na celu anulowania głosów za, których nie lubisz, tak jak za głosy przeciwne, których nie lubisz. Jeśli ludzie uważają, że pytanie można poprawić, z pewnością osoba zadająca pytanie po raz pierwszy powinna otrzymać informację zwrotną. Nie widzę powodu, by głosować przeciw, ale chciałbym wiedzieć, dlaczego inni to robią, chociaż mogą tego nie powiedzieć.
Shafik Yaghmour

Odpowiedzi:


159

Deklaracja nie jest błędem float a = 3.0: jeśli to zrobisz, kompilator przekonwertuje podwójny literał 3.0 na zmiennoprzecinkowy.


Jednak w określonych scenariuszach należy używać notacji literałów zmiennoprzecinkowych.

  1. Ze względu na wydajność:

    W szczególności rozważ:

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

    W tym przypadku kompilator wyemituje konwersję (którą zapłacisz w czasie wykonywania) dla każdej zwróconej wartości. Aby tego uniknąć, należy zadeklarować:

    float foo(float x) { return x * 0.42f; } // OK, no conversion required
    
  2. Aby uniknąć błędów podczas porównywania wyników:

    np. poniższe porównanie zawodzi:

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

    Możemy to naprawić za pomocą dosłownej notacji float:

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

    (Uwaga: oczywiście nie w ten sposób należy porównywać liczby zmiennoprzecinkowe lub podwójne dla równości w ogóle )

  3. Aby wywołać poprawną przeciążoną funkcję (z tego samego powodu):

    Przykład:

    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. Jak zauważył Cyber , w kontekście dedukcji typu należy pomóc kompilatorowi wydedukować float:

    W przypadku auto:

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

    I podobnie w przypadku odliczenia typu szablonu:

    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 na żywo


2
W punkcie 1 42jest liczbą całkowitą, która jest automatycznie promowana do float(i stanie się to w czasie kompilacji w każdym porządnym kompilatorze), więc nie ma spadku wydajności. Prawdopodobnie miałeś na myśli coś takiego 42.0.
Matteo Italia

@MatteoItalia, tak, miałem na myśli 42,0 ofc (edytowane, dzięki)
quantdev

2
@ChristianHackl Konwersja 4.2na 4.2fmoże mieć efekt uboczny ustawienia FE_INEXACTflagi, w zależności od kompilatora i systemu, a niektóre (co prawda nieliczne) programy dbają o to, które operacje zmiennoprzecinkowe są dokładne, a które nie, i testują tę flagę . Oznacza to, że prosta i oczywista transformacja w czasie kompilacji zmienia zachowanie programu.

6
float foo(float x) { return x*42.0; }można skompilować do mnożenia z pojedynczą precyzją i został skompilowany tak przez Clanga, kiedy ostatnio próbowałem. Jednak float foo(float x) { return x*0.1; }nie można go skompilować do pojedynczego mnożenia o pojedynczej precyzji. To mogło być trochę zbyt optymistyczne przed tą łatką, ale po łatce powinno łączyć konwersję-double_precision_op-conversion do single_precision_op tylko wtedy, gdy wynik jest zawsze taki sam. article.gmane.org/gmane.comp.compilers.llvm.cvs/167800/match=
Pascal Cuoq

1
Jeśli ktoś chce obliczyć wartość, która jest jedną dziesiątą someFloat, wyrażenie someFloat * 0.1da dokładniejsze wyniki niż someFloat * 0.1f, chociaż w wielu przypadkach jest tańsze niż dzielenie zmiennoprzecinkowe. Na przykład (float) (167772208.0f * 0.1) zostanie poprawnie zaokrąglone do 16777220 zamiast 16777222. Niektóre kompilatory mogą podstawiać doublemnożenie dla dzielenia zmiennoprzecinkowego, ale dla tych, które tego nie robią (jest to bezpieczne dla wielu, choć nie dla wszystkich wartości ) mnożenie może być użyteczną optymalizacją, ale tylko wtedy, gdy jest wykonywane z doubleodwrotnością.
supercat,

22

Kompilator zamieni dowolny z poniższych literałów na zmiennoprzecinkowe, ponieważ zadeklarowałeś zmienną jako zmiennoprzecinkową.

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

Miałoby to znaczenie, gdybyś użył auto(lub innych metod odliczania), na przykład:

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

5
Typy są również wywnioskowane podczas korzystania z szablonów, więc autonie jest to jedyny przypadek.
Shafik Yaghmour,

14

Literały zmiennoprzecinkowe bez sufiksu są typu double , jest to omówione w sekcji standardowej wersji roboczej C ++2.14.4 Literały zmiennoprzecinkowe :

[...] Typ zmiennego literału jest podwójny, chyba że wyraźnie określono go sufiksem. [...]

więc jest to błąd, aby przypisać 3.0do podwójnego dosłownym do pływaka ?:

float a = 3.0

Nie, nie jest, zostanie przekonwertowany, co jest omówione w sekcji 4.8 Konwersje zmiennoprzecinkowe :

Wartość pr typu zmiennoprzecinkowego może zostać przekonwertowana na wartość pr innego typu zmiennoprzecinkowego. Jeśli wartość źródłowa może być dokładnie przedstawiona w typie docelowym, wynikiem konwersji jest ta dokładna reprezentacja. Jeśli wartość źródłowa znajduje się między dwiema sąsiednimi wartościami docelowymi, wynikiem konwersji jest zdefiniowany w implementacji wybór jednej z tych wartości. W przeciwnym razie zachowanie jest niezdefiniowane.

Więcej szczegółów na temat konsekwencji tego możemy przeczytać w GotW # 67: double or none, który mówi:

Oznacza to, że podwójna stała może być niejawnie (tj. Po cichu) przekonwertowana na stałą zmiennoprzecinkową, nawet jeśli spowoduje to utratę precyzji (tj. Danych). Pozwolono na to ze względu na kompatybilność z C i użyteczność, ale warto o tym pamiętać podczas wykonywania operacji zmiennoprzecinkowych.

Kompilator wysokiej jakości ostrzeże cię, jeśli spróbujesz zrobić coś, co jest niezdefiniowane, a mianowicie umieścić podwójną ilość w zmiennej zmiennoprzecinkowej, która jest mniejsza niż minimalna lub większa niż maksymalna wartość, którą jest w stanie przedstawić. Naprawdę dobry kompilator wyświetli opcjonalne ostrzeżenie, jeśli spróbujesz zrobić coś, co może być zdefiniowane, ale może utracić informacje, a mianowicie umieścić podwójną ilość w zmiennej zmiennoprzecinkowej, która znajduje się między minimalną a maksymalną wartością reprezentowaną przez zmiennoprzecinkową, ale której nie może być reprezentowane dokładnie jako liczba zmiennoprzecinkowa.

Są więc zastrzeżenia dotyczące ogólnego przypadku, o których powinieneś wiedzieć.

Z praktycznego punktu widzenia, w tym przypadku wyniki będą najprawdopodobniej takie same, mimo że technicznie istnieje konwersja, możemy to zobaczyć, wypróbowując następujący kod na 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;
}

i widzimy, że wyniki dla func1i func2są identyczne przy użyciu obu clangi gcc:

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

Jak podkreśla Pascal w tym komentarzu , nie zawsze będziesz mógł na to liczyć. Użycie 0.1i 0.1fodpowiednio powoduje, że wygenerowany zestaw różni się, ponieważ konwersja musi być teraz wykonana jawnie. Poniższy kod:

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

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

skutkuje następującym montażem:

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

Niezależnie od tego, czy możesz określić, czy konwersja będzie miała wpływ na wydajność, czy nie, użycie właściwego typu lepiej dokumentuje Twoje zamiary. Na przykład użycie jawnych konwersji static_castpomaga również wyjaśnić, że konwersja była zamierzona, a nie przypadkowa, co może oznaczać błąd lub potencjalny błąd.

Uwaga

Jak wskazuje supercat, mnożenie przez np. 0.1I 0.1fnie jest równoważne. Zacytuję tylko komentarz, ponieważ był doskonały, a podsumowanie prawdopodobnie nie oddałoby tego sprawiedliwie:

Na przykład, jeśli f było równe 100000224 (co jest dokładnie reprezentowalne jako liczba zmiennoprzecinkowa), pomnożenie go przez jedną dziesiątą powinno dać wynik, który zaokrągla w dół do 10000022, ale pomnożenie przez 0,1 f da zamiast tego wynik, który błędnie zaokrągla do 10000023 Jeśli intencją jest podzielenie przez dziesięć, pomnożenie przez podwójną stałą 0,1 będzie prawdopodobnie szybsze niż dzielenie przez 10f i dokładniejsze niż mnożenie przez 0,1f.

Moim pierwotnym celem było zademonstrowanie fałszywego przykładu podanego w innym pytaniu, ale to doskonale pokazuje, że w przykładach zabawek mogą występować subtelne problemy.


1
Warto zauważyć, że wyrażenia f = f * 0.1;i f = f * 0.1f; robią różne rzeczy . Na przykład, jeśli fbyło równe 100000224 (co jest dokładnie reprezentowane jako a float), pomnożenie go przez jedną dziesiątą powinno dać wynik, który zaokrągla się w dół do 10000022, ale pomnożenie przez 0,1f da wynik, który błędnie zaokrągla w górę do 10000023. Jeśli intencją jest podzielenie przez dziesięć, mnożenie przez doublestałą 0,1 będzie prawdopodobnie szybsze niż dzielenie przez 10fi dokładniejsze niż mnożenie przez 0.1f.
supercat

@supercat dziękuję za miły przykład, zacytowałem Cię bezpośrednio, nie krępuj się edytować według własnego uznania.
Shafik Yaghmour

4

Nie jest to błąd w tym sensie, że kompilator go odrzuci, ale jest to błąd w tym sensie, że może nie być tym, czego chcesz.

Jak słusznie stwierdza twoja książka, 3.0to wartość typu double. Istnieje niejawna konwersja z doublena float, więc float a = 3.0;jest to poprawna definicja zmiennej.

Jednak przynajmniej koncepcyjnie powoduje to niepotrzebną konwersję. W zależności od kompilatora konwersja może zostać przeprowadzona w czasie kompilacji lub może zostać zapisana na czas wykonywania. Ważnym powodem zapisania go w czasie wykonywania jest to, że konwersje zmiennoprzecinkowe są trudne i mogą mieć nieoczekiwane skutki uboczne, jeśli wartość nie może być dokładnie odwzorowana i nie zawsze jest łatwo zweryfikować, czy wartość może być dokładnie reprezentowana.

3.0f unika tego problemu: chociaż technicznie kompilator nadal może obliczyć stałą w czasie wykonywania (tak jest zawsze), tutaj nie ma absolutnie żadnego powodu, dla którego jakikolwiek kompilator mógłby to zrobić.


Rzeczywiście, w przypadku kompilatora krzyżowego byłoby całkiem niepoprawne wykonanie konwersji w czasie kompilacji, ponieważ miałoby to miejsce na złej platformie.
Markiz Lorne

2

Chociaż nie jest to błąd, sam w sobie jest trochę niechlujny. Wiesz, że chcesz float, więc zainicjuj go float.
Może przyjść inny programista i nie będzie pewien, która część deklaracji jest poprawna, typ lub inicjator. Dlaczego nie mieliby obu mieć racji?
float Answer = 42.0f;


0

Podczas definiowania zmiennej jest inicjowana za pomocą dostarczonego inicjatora. Może to wymagać konwersji wartości inicjatora do typu inicjowanej zmiennej. To właśnie się dzieje, gdy mówisz float a = 3.0;: Wartość inicjatora jest konwertowana na float, a wynik konwersji staje się wartością początkową a.

Ogólnie jest to w porządku, ale nie zaszkodzi pisać, 3.0faby pokazać, że jesteś świadomy tego, co robisz, a zwłaszcza jeśli chcesz pisać auto a = 3.0f.


0

Jeśli spróbujesz następujących rzeczy:

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

otrzymasz wynik jako:

4:8

To pokazuje, że rozmiar 3.2f jest traktowany jako 4 bajty na komputerze 32-bitowym, gdzie 3.2 jest interpretowany jako podwójna wartość zajmująca 8 bajtów na komputerze 32-bitowym. Powinno to dostarczyć odpowiedzi, której szukasz.


To pokazuje, że doublei floatsą inne, nie odpowiada, czy możesz zainicjować a floatz podwójnego dosłownego
Jonathan Wakely

oczywiście możesz zainicjować zmiennoprzecinkową z podwójnej wartości, z zastrzeżeniem obcięcia danych, jeśli dotyczy
Dr. Debasish Jana,

4
Tak, wiem, ale to było pytanie OP, więc twoja odpowiedź nie udzieliła na nie odpowiedzi, mimo że twierdził, że udzieliła odpowiedzi!
Jonathan Wakely,

0

Kompilator wyprowadza najlepiej dopasowany typ z literałów lub przynajmniej tego, co uważa za najlepiej pasujące. Oznacza to raczej utratę wydajności w porównaniu z precyzją, tj. Użyj double zamiast float. W razie wątpliwości użyj nawiasów intializujących, aby wyraźnie to zaznaczyć:

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

Historia staje się bardziej interesująca, jeśli zainicjujesz z innej zmiennej, do której mają zastosowanie reguły konwersji typów: Chociaż tworzenie podwójnej formy literału jest legalne, nie można jej utworzyć z int bez możliwości zawężenia:

auto xxx = double{i} // warning ! narrowing conversion of 'i' from 'int' to 'double' 
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.