Якщо я маю таку декларацію:
float a = 3.0 ;
це помилка? Я читав у книзі, яка 3.0
є double
цінністю, і яку я повинен вказати як float a = 3.0f
. Це так?
Якщо я маю таку декларацію:
float a = 3.0 ;
це помилка? Я читав у книзі, яка 3.0
є double
цінністю, і яку я повинен вказати як float a = 3.0f
. Це так?
;
після.
Відповіді:
Це не помилка float a = 3.0
: якщо ви це зробите, компілятор перетворить подвійний літерал 3.0 на плаваючу для вас.
Однак вам слід використовувати позначення плаваючих літералів у конкретних сценаріях.
З міркувань ефективності:
Зокрема, розглянемо:
float foo(float x) { return x * 0.42; }
Тут компілятор видасть конверсію (яку ви заплатите під час виконання) для кожного поверненого значення. Щоб уникнути цього, слід заявити:
float foo(float x) { return x * 0.42f; } // OK, no conversion required
Щоб уникнути помилок при порівнянні результатів:
наприклад, наступне порівняння не вдається:
float x = 4.2;
if (x == 4.2)
std::cout << "oops"; // Not executed!
Ми можемо виправити це за допомогою плавального буквального позначення:
if (x == 4.2f)
std::cout << "ok !"; // Executed!
(Примітка: звичайно, це не те, як слід порівнювати плаваючі або подвійні числа для рівності загалом )
Щоб викликати правильну перевантажену функцію (з тієї ж причини):
Приклад:
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;
}
Як зазначає Cyber , у контексті вирахування типу необхідно допомогти компілятору вивести afloat
:
У разі auto
:
auto d = 3; // int
auto e = 3.0; // double
auto f = 3.0f; // float
І так само, у разі вирахування типу шаблону:
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
- ціле число, яке автоматично підвищується до float
(і це відбуватиметься під час компіляції в будь-якому гідному компіляторі), тому покарання за продуктивність не передбачено. Напевно, ви мали на увазі щось подібне 42.0
.
4.2
на 4.2f
може мати побічний ефект від встановлення FE_INEXACT
прапора залежно від компілятора та системи, а деякі (правда, небагато) програми дбають про те, які операції з плаваючою точкою є точними, а які ні, і перевіряють цей прапор . Це означає, що проста очевидна трансформація під час компіляції змінює поведінку програми.
float foo(float x) { return x*42.0; }
може бути скомпільовано до множення з однією точністю, і був скомпільований таким чином Clang востаннє, коли я намагався. Однак float foo(float x) { return x*0.1; }
не може бути скомпільоване до одного множення з однією точністю. Можливо, він був трохи надмірно оптимістичним до цього виправлення, але після виправлення він повинен поєднувати convert-double_precision_op-convert до single_precision_op, коли результат завжди однаковий. article.gmane.org/gmane.comp.compilers.llvm.cvs/167800/match=
someFloat
, вираз someFloat * 0.1
отримає точніші результати, ніж someFloat * 0.1f
, хоча в багатьох випадках є дешевшим, ніж ділення з плаваючою точкою. Наприклад, (float) (167772208.0f * 0.1) буде правильно округляти до 16777220, а не до 16777222. Деякі компілятори можуть замінити double
множення на ділення з плаваючою комою, але для тих, що цього не роблять (це безпечно для багатьох, хоча і не всіх значень ) множення може бути корисною оптимізацією, але лише за умови, що воно виконується із double
взаємністю.
Компілятор перетворить будь-який з наступних літералів на плаваючі, оскільки ви оголосили змінну як плаваючу.
float a = 3; // converted to float
float b = 3.0; // converted to float
float c = 3.0f; // float
Було б важливо, якщо ви використовували auto
(або інші методи вирахування типів), наприклад:
auto d = 3; // int
auto e = 3.0; // double
auto f = 3.0f; // float
auto
це не єдиний випадок.
Літерали з плаваючою комою без суфікса мають тип double , це висвітлено у проекті стандартного розділу C ++ 2.14.4
Плаваючі літерали :
[...] Тип плаваючого літералу подвійний, якщо це явно не вказано суфіксом. [...]
так що це помилка , щоб призначити 3.0
на подвійний Літерал до поплавця :
float a = 3.0
Ні, це не так, він буде перетворений, що описано в розділі 4.8
Перетворення з плаваючою комою :
Перша величина типу плаваючої крапки може бути перетворена на першу величину іншого типу плаваючої крапки. Якщо вихідне значення може бути точно представлено в типі призначення, результатом перетворення є саме це подання. Якщо вихідне значення знаходиться між двома сусідніми цільовими значеннями, результатом перетворення є вибір, визначений реалізацією, будь-якого з цих значень. В іншому випадку поведінка не визначена.
Ми можемо прочитати більше подробиць про наслідки цього в GotW # 67: подвійний чи нічого, що говорить:
Це означає, що подвійну константу можна неявно (тобто мовчки) перетворити на плаваючу константу, навіть якщо це втрачає точність (тобто дані). Це було дозволено зберегти з міркувань сумісності та зручності використання, але варто пам’ятати, коли ви виконуєте роботу з плаваючою комою.
Якісний компілятор попередить вас, якщо ви спробуєте зробити щось із невизначеною поведінкою, а саме вкладіть подвійну величину в поплавок, менший за мінімальне або більший за максимальне значення, яке здатний представляти поплавок. Дійсно хороший компілятор подасть необов’язкове попередження, якщо ви спробуєте зробити щось, що може бути визначеним, але може втратити інформацію, а саме ввести подвійну величину в поплавок, який знаходиться між мінімальним і максимальним значеннями, представленими поплавком, але який не може бути представленим точно як поплавок.
Тож є застереження щодо загального випадку, про які ви повинні знати.
З практичної точки зору, у цьому випадку результати, швидше за все, будуть однаковими, навіть незважаючи на те, що технічно відбувається перетворення, ми можемо переконатися в цьому, спробувавши наступний код на 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;
}
і ми бачимо, що результати для func1
і func2
ідентичні, використовуючи обидва clang
і gcc
:
func1():
movss xmm0, DWORD PTR .LC0[rip]
ret
func2():
movss xmm0, DWORD PTR .LC0[rip]
ret
Як зазначає Паскаль у цьому коментарі, ви не завжди зможете на це розраховувати. Використання 0.1
та, 0.1f
відповідно, спричиняє різницю згенерованої збірки, оскільки перетворення тепер повинно виконуватися явно. Наступний код:
float func1(float x )
{
return x*0.1; // a double literal
}
float func2(float x)
{
return x*0.1f ; // a float literal
}
призводить до наступного складання:
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
Незалежно від того, чи зможете ви визначити, чи перетворення матиме вплив на ефективність чи ні, використовуючи правильний тип, краще документуйте ваш намір. Наприклад, використання явних перетворень static_cast
також допомагає уточнити, що перетворення було призначене на відміну від випадкового, що може означати помилку або потенційну помилку.
Примітка
Як зазначає supercat, множення на eg 0.1
і 0.1f
не є рівнозначним. Я просто цитую коментар, тому що він був чудовим, і резюме, мабуть, не зробить це справедливим:
Наприклад, якщо f дорівнювало 100000224 (що точно представляється як плаваюче), помноживши його на одну десяту, слід отримати результат, який округлюється до 10000022, але, натомість множення на 0,1f дасть результат, який помилково округлює до 10000023 Якщо метою є ділення на десять, множення на подвійну константу 0,1, швидше за все, буде швидшим, ніж ділення на 10f, і точніше, ніж множення на 0,1f.
Моїм початковим завданням було продемонструвати помилковий приклад, наведений в іншому питанні, але це чітко демонструє тонкі проблеми, які можуть існувати у прикладах іграшок.
f = f * 0.1;
і f = f * 0.1f;
роблять різні речі . Наприклад, якщо f
було рівне 100000224 (що точно представляється як a float
), помноживши його на одну десяту, слід отримати результат, який округлюється до 10000022, але, натомість множення на 0,1f дасть результат, який помилково округляє до 10000023. Якщо намір полягає в діленні на десять, множення на double
константу 0,1, швидше за все, буде швидшим, ніж ділення на 10f
, і точніше, ніж множення на 0.1f
.
Це не помилка в тому сенсі, що компілятор відхилить її, але це помилка в тому сенсі, що це може бути не те, що ви хочете.
Як правильно вказано у вашій книзі, 3.0
це значення типу double
. Існує неявне перетворення з double
в float
, отжеfloat a = 3.0;
є і дійсне визначення змінної.
Однак, принаймні концептуально, це робить непотрібне перетворення. Залежно від компілятора, перетворення може виконуватися під час компіляції, або може зберігатися на час виконання. Дійсною причиною збереження його під час виконання є те, що перетворення з плаваючою комою є складним і може мати несподівані побічні ефекти, якщо значення неможливо точно представити, і не завжди легко перевірити, чи можна точно представити значення.
3.0f
уникає цієї проблеми: хоча технічно компілятору все ще дозволено обчислювати константу під час виконання (вона завжди є), тут абсолютно немає причин, чому будь-який компілятор може це робити.
Хоча сама по собі це не помилка, вона трохи неакуратна. Ви знаєте, що хочете флоат, тому ініціалізуйте його флоатом.
Може підійти інший програміст і не бути впевненим, яка частина декларації є правильною, тип чи ініціалізатор. Чому б їм обом не бути правильними?
float Відповідь = 42.0f;
Коли ви визначаєте змінну, вона ініціалізується за допомогою наданого ініціалізатора. Для цього може знадобитися перетворення значення ініціалізатора у тип змінної, яка ініціюється. Ось що відбувається, коли ви говорите float a = 3.0;
: значення ініціалізатора перетворюється на float
, а результат перетворення стає початковим значенням a
.
Це, як правило, добре, але не заважає писати, 3.0f
щоб показати, що ви усвідомлюєте те, що робите, особливо, якщо хочете писати auto a = 3.0f
.
Якщо ви спробуєте наступне:
std::cout << sizeof(3.2f) <<":" << sizeof(3.2) << std::endl;
Ви отримаєте результат як:
4:8
що показує, розмір 3.2f приймається як 4 байти на 32-бітній машині, де 3.2 інтерпретується як подвійне значення, що приймає 8 байт на 32-бітній машині. Це має дати відповідь, яку ви шукаєте.
double
і float
відрізняються, це не відповідає, чи можна ініціалізувати a float
з подвійного літералу
Компілятор виводить найбільш підходящий тип з літералів, або, принаймні, те, що він вважає найкращим. Це швидше втрачає ефективність порівняно з точністю, тобто використовує подвійний замість поплавця. Якщо ви сумніваєтеся, скористайтеся інструментами для підтяжки, щоб зробити це явним:
auto d = double{3}; // make a double
auto f = float{3}; // make a float
auto i = int{3}; // make a int
Історія стає цікавішою, якщо ініціалізувати з іншої змінної, де застосовуються правила перетворення типів: Хоча дозволено створювати подвійну форму як літерал, її не можна створювати з int без можливого звуження:
auto xxx = double{i} // warning ! narrowing conversion of 'i' from 'int' to 'double'
3.0
на плаваючу для вас. Кінцевий результат не відрізняється відfloat a = 3.0f
.