Если у меня есть следующее заявление:
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!
Мы можем исправить это с помощью буквального обозначения float:
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 , в контексте вывода типа необходимо помочь компилятору вывести 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;
}
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; }не может быть скомпилирован до одинарного умножения с одинарной точностью. Возможно, до этого патча это было немного чересчур оптимистично, но после патча следует комбинировать преобразование-double_precision_op-conversion с 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 Плавающие литералы :
[...] Тип плавающего литерала - 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.
Моя первоначальная цель заключалась в том, чтобы продемонстрировать ложный пример, приведенный в другом вопросе, но это прекрасно демонстрирует, что тонкие проблемы могут существовать в игрушечных примерах.
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 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они разные, но не отвечает, можно ли инициализировать floata двойным литералом
Компилятор выводит наиболее подходящий тип из литералов или, по крайней мере, из того, что он считает наиболее подходящим. Это скорее потеря эффективности по сравнению с точностью, то есть использование 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'
3.0в число с плавающей запятой. Конечный результат неотличим отfloat a = 3.0f.