Is '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: Да, но это не имеет особого отношения к этому вопросу, который касается правил C ++.
Кейт Томпсон
20
Ну, по крайней мере, нужно ;после.
Hot Licks
3
10 голосов против и не так много в комментариях, чтобы их объяснить, что очень обескураживает. Это первый вопрос ОП, и если люди считают, что это стоит 10 отрицательных голосов, должны быть некоторые объяснения. Это правильный вопрос с неочевидными последствиями, и многие интересные вещи можно узнать из ответов и комментариев.
Шафик Ягмур
3
@HotLicks - это не о плохом или хорошем самочувствии, конечно, это может показаться несправедливым, но это жизнь, в конце концов, это очки единорога. Дау-голоса, безусловно, не должны отменять голоса за, которые вам не нравятся, точно так же, как положительные голоса не отменяют отрицательные голоса, которые вам не нравятся. Если люди считают, что вопрос можно улучшить, конечно, тот, кто впервые задает вопрос, должен получить обратную связь. Я не вижу причин для отрицательного голоса, но я хотел бы знать, почему это делают другие, хотя они могут не говорить этого.
Шафик Ягмур

Ответы:

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!
    

    Мы можем исправить это с помощью буквального обозначения float:

    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 , в контексте вывода типа необходимо помочь компилятору вывести float:

    В случае 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;
    }
    

Живая демонстрация

Quantdev
источник
2
В пункте 1 42находится целое число, которое автоматически повышается до float(и это произойдет во время компиляции в любом достойном компиляторе), поэтому нет потери производительности. Наверное, вы имели в виду что-то вроде 42.0.
Маттео Италия
@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; }не может быть скомпилирован до одинарного умножения с одинарной точностью. Возможно, до этого патча это было немного чересчур оптимистично, но после патча следует комбинировать преобразование-double_precision_op-conversion с 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 Плавающие литералы :

[...] Тип плавающего литерала - double, если явно не указан суффикс. [...]

так что это ошибка , чтобы назначить 3.0на двойной Литерал к поплавку :

float a = 3.0

Нет, он будет преобразован, что описано в разделе 4.8 Преобразования с плавающей запятой :

Prvalue типа с плавающей запятой может быть преобразовано в prvalue другого типа с плавающей запятой. Если исходное значение может быть точно представлено в целевом типе, результатом преобразования будет это точное представление. Если исходное значение находится между двумя соседними значениями назначения, результатом преобразования является выбор любого из этих значений, определяемый реализацией. В противном случае поведение не определено.

Мы можем прочитать более подробную информацию о последствиях этого в GotW # 67: дважды или ничего, в котором говорится:

Это означает, что двойная константа может быть неявно (т. Е. Незаметно) преобразована в константу с плавающей запятой, даже если при этом теряется точность (т. Е. Данные). Это было разрешено оставить из соображений совместимости с C и удобства использования, но об этом стоит помнить при работе с плавающей запятой.

Качественный компилятор предупредит вас, если вы попытаетесь сделать что-то неопределенное, а именно поместить двойное количество в число с плавающей запятой, которое меньше минимального или больше максимального значения, которое может представлять число с плавающей запятой. Действительно хороший компилятор предоставит необязательное предупреждение, если вы попытаетесь сделать что-то, что может быть определено, но может потерять информацию, а именно поместить двойное количество в число с плавающей запятой, которое находится между минимальным и максимальным значениями, представляемыми с помощью числа с плавающей точкой, но которое не может быть представленным в точности как поплавок.

Так что есть предостережения для общего случая, о которых вам следует знать.

С практической точки зрения, в этом случае результаты, скорее всего, будут такими же, даже если технически есть преобразование, мы можем убедиться в этом, попробовав следующий код на 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, поэтому инициализируйте его с помощью float.
Другой программист может прийти и не быть уверенным, какая часть объявления верна, тип или инициализатор. Почему они оба не верны?
float Answer = 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они разные, но не отвечает, можно ли инициализировать floata двойным литералом
Джонатан Уэйкли
конечно, вы можете инициализировать число с плавающей запятой из двойного значения с усечением данных, если это применимо
Д-р Дебасиш Яна
4
Да, я знаю, но это был вопрос ОП, так что ваш ответ на самом деле не отвечал на него, несмотря на то, что я заявлял, что дает ответ!
Джонатан Уэйкли
0

Компилятор выводит наиболее подходящий тип из литералов или, по крайней мере, из того, что он считает наиболее подходящим. Это скорее потеря эффективности по сравнению с точностью, то есть использование double вместо 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' 
трущивальный
источник