Jeśli mam następującą deklarację:
float a = 3.0 ;
czy to błąd? Przeczytałem w książce, która 3.0
jest double
wartością i muszę ją określić jako float a = 3.0f
. Czy tak jest?
Jeśli mam następującą deklarację:
float a = 3.0 ;
czy to błąd? Przeczytałem w książce, która 3.0
jest double
wartością i muszę ją określić jako float a = 3.0f
. Czy tak jest?
;
później.
Odpowiedzi:
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.
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
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 )
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;
}
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;
}
42
jest 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
.
4.2
na 4.2f
może mieć efekt uboczny ustawienia FE_INEXACT
flagi, 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.
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=
someFloat
, wyrażenie someFloat * 0.1
da 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ć double
mnoż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 double
odwrotnością.
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
auto
nie jest to jedyny przypadek.
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.0
do 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 func1
i func2
są identyczne przy użyciu obu clang
i 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.1
i 0.1f
odpowiednio 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_cast
pomaga 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.1
I 0.1f
nie 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.
f = f * 0.1;
i f = f * 0.1f;
robią różne rzeczy . Na przykład, jeśli f
był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 double
stałą 0,1 będzie prawdopodobnie szybsze niż dzielenie przez 10f
i dokładniejsze niż mnożenie przez 0.1f
.
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.0
to wartość typu double
. Istnieje niejawna konwersja z double
na 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ć.
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.0f
aby pokazać, że jesteś świadomy tego, co robisz, a zwłaszcza jeśli chcesz pisać auto a = 3.0f
.
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.
double
i float
są inne, nie odpowiada, czy możesz zainicjować a float
z podwójnego dosłownego
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'
3.0
w zmiennoprzecinkowy. Wynik końcowy jest nie do odróżnienia odfloat a = 3.0f
.