Є 'float a = 3.0;' правильне твердження?


86

Якщо я маю таку декларацію:

float a = 3.0 ;

це помилка? Я читав у книзі, яка 3.0є doubleцінністю, і яку я повинен вказати як float a = 3.0f. Це так?


2
Компілятор перетворить подвійний літерал 3.0на плаваючу для вас. Кінцевий результат не відрізняється від float a = 3.0f.
Девід Хеффернан

6
@EdHeal: Це так, але це не особливо стосується цього питання, яке стосується правил С ++.
Кіт Томпсон,

20
Ну, принаймні вам потрібно ;після.
Hot Licks

3
10 голосів проти і не так багато коментарів, щоб пояснити їх, дуже знеохочує. Це перше питання ОП, і якщо люди вважають, що це коштує 10 голосів проти, мають бути деякі пояснення. Це слушне запитання з неочевидними наслідками та багато цікавого, що можна дізнатись із відповідей та коментарів.
Шафік Ягмор

3
@HotLicks мова не про те, щоб почуватись погано чи добре, впевнений, що це може здатися несправедливим, але це життя, це все-таки очки єдинорога. Прихильники Dowvotes, безсумнівно, не скасовують голосів, які вам не подобаються, так само, як upvotes не відміняють негативних, які вам не подобаються. Якщо люди вважають, що питання можна покращити, напевно, хто вперше запитує, повинен отримати якийсь відгук. Я не бачу підстав для того, щоб голосувати проти, але я хотів би знати, чому так роблять інші, хоча вони вільні цього не говорити.
Шафік Ягмор

Відповіді:


159

Це не помилка float a = 3.0: якщо ви це зробите, компілятор перетворить подвійний літерал 3.0 на плаваючу для вас.


Однак вам слід використовувати позначення плаваючих літералів у конкретних сценаріях.

  1. З міркувань ефективності:

    Зокрема, розглянемо:

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

    Тут компілятор видасть конверсію (яку ви заплатите під час виконання) для кожного поверненого значення. Щоб уникнути цього, слід заявити:

    float foo(float x) { return x * 0.42f; } // OK, no conversion required
    
  2. Щоб уникнути помилок при порівнянні результатів:

    наприклад, наступне порівняння не вдається:

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

    Ми можемо виправити це за допомогою плавального буквального позначення:

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

    (Примітка: звичайно, це не те, як слід порівнювати плаваючі або подвійні числа для рівності загалом )

  3. Щоб викликати правильну перевантажену функцію (з тієї ж причини):

    Приклад:

    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. Як зазначає 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;
    }
    

Демо в прямому ефірі


2
У пункті 1 42- ціле число, яке автоматично підвищується до float(і це відбуватиметься під час компіляції в будь-якому гідному компіляторі), тому покарання за продуктивність не передбачено. Напевно, ви мали на увазі щось подібне 42.0.
Matteo Italia

@MatteoItalia, так, я мав на увазі 42.0 ofc (відредаговано, дякую)
quantdev

2
@ChristianHackl Перетворення 4.2на 4.2fможе мати побічний ефект від встановлення FE_INEXACTпрапора залежно від компілятора та системи, а деякі (правда, небагато) програми дбають про те, які операції з плаваючою точкою є точними, а які ні, і перевіряють цей прапор . Це означає, що проста очевидна трансформація під час компіляції змінює поведінку програми.

6
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=
Паскаль Куок

1
Якщо хочеться обчислити значення, яке становить одну десяту someFloat, вираз someFloat * 0.1отримає точніші результати, ніж someFloat * 0.1f, хоча в багатьох випадках є дешевшим, ніж ділення з плаваючою точкою. Наприклад, (float) (167772208.0f * 0.1) буде правильно округляти до 16777220, а не до 16777222. Деякі компілятори можуть замінити doubleмноження на ділення з плаваючою комою, але для тих, що цього не роблять (це безпечно для багатьох, хоча і не всіх значень ) множення може бути корисною оптимізацією, але лише за умови, що воно виконується із doubleвзаємністю.
supercat

22

Компілятор перетворить будь-який з наступних літералів на плаваючі, оскільки ви оголосили змінну як плаваючу.

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

5
Типи також визначаються при використанні шаблонів, тому autoце не єдиний випадок.
Шафік Ягмур

14

Літерали з плаваючою комою без суфікса мають тип 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.

Моїм початковим завданням було продемонструвати помилковий приклад, наведений в іншому питанні, але це чітко демонструє тонкі проблеми, які можуть існувати у прикладах іграшок.


1
Може бути, варто зазначити, що вирази f = f * 0.1;і f = f * 0.1f; роблять різні речі . Наприклад, якщо fбуло рівне 100000224 (що точно представляється як a float), помноживши його на одну десяту, слід отримати результат, який округлюється до 10000022, але, натомість множення на 0,1f дасть результат, який помилково округляє до 10000023. Якщо намір полягає в діленні на десять, множення на doubleконстанту 0,1, швидше за все, буде швидшим, ніж ділення на 10f, і точніше, ніж множення на 0.1f.
supercat

@supercat дякую за приємний приклад, я цитував вас безпосередньо, будь ласка, редагуйте так, як вважаєте за потрібне.
Шафік Ягмор

4

Це не помилка в тому сенсі, що компілятор відхилить її, але це помилка в тому сенсі, що це може бути не те, що ви хочете.

Як правильно вказано у вашій книзі, 3.0це значення типу double. Існує неявне перетворення з doubleв float, отжеfloat a = 3.0; є і дійсне визначення змінної.

Однак, принаймні концептуально, це робить непотрібне перетворення. Залежно від компілятора, перетворення може виконуватися під час компіляції, або може зберігатися на час виконання. Дійсною причиною збереження його під час виконання є те, що перетворення з плаваючою комою є складним і може мати несподівані побічні ефекти, якщо значення неможливо точно представити, і не завжди легко перевірити, чи можна точно представити значення.

3.0f уникає цієї проблеми: хоча технічно компілятору все ще дозволено обчислювати константу під час виконання (вона завжди є), тут абсолютно немає причин, чому будь-який компілятор може це робити.


Дійсно, у випадку перехресного компілятора було б зовсім некоректно виконувати перетворення під час компіляції, оскільки це відбувалося б на неправильній платформі.
Маркіз Лорнський

2

Хоча сама по собі це не помилка, вона трохи неакуратна. Ви знаєте, що хочете флоат, тому ініціалізуйте його флоатом.
Може підійти інший програміст і не бути впевненим, яка частина декларації є правильною, тип чи ініціалізатор. Чому б їм обом не бути правильними?
float Відповідь = 42.0f;


0

Коли ви визначаєте змінну, вона ініціалізується за допомогою наданого ініціалізатора. Для цього може знадобитися перетворення значення ініціалізатора у тип змінної, яка ініціюється. Ось що відбувається, коли ви говорите float a = 3.0;: значення ініціалізатора перетворюється на float, а результат перетворення стає початковим значенням a.

Це, як правило, добре, але не заважає писати, 3.0fщоб показати, що ви усвідомлюєте те, що робите, особливо, якщо хочете писати auto a = 3.0f.


0

Якщо ви спробуєте наступне:

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

Ви отримаєте результат як:

4:8

що показує, розмір 3.2f приймається як 4 байти на 32-бітній машині, де 3.2 інтерпретується як подвійне значення, що приймає 8 байт на 32-бітній машині. Це має дати відповідь, яку ви шукаєте.


Це показує, що doubleі floatвідрізняються, це не відповідає, чи можна ініціалізувати a floatз подвійного літералу
Джонатан Уейклі

звичайно, ви можете ініціалізувати float із подвійного значення за умови обрізання даних, якщо це можливо
доктор Дебашіш Яна

4
Так, я знаю, але це було питання OP, тож ваша відповідь насправді не змогла відповісти на нього, незважаючи на вимоги надати відповідь!
Джонатан Уейклі

0

Компілятор виводить найбільш підходящий тип з літералів, або, принаймні, те, що він вважає найкращим. Це швидше втрачає ефективність порівняно з точністю, тобто використовує подвійний замість поплавця. Якщо ви сумніваєтеся, скористайтеся інструментами для підтяжки, щоб зробити це явним:

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' 
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.