Компилятор преобразует за вас двойной литерал 3.0в число с плавающей запятой. Конечный результат неотличим от float a = 3.0f.
Дэвид Хеффернан
6
@EdHeal: Да, но это не имеет особого отношения к этому вопросу, который касается правил C ++.
Кейт Томпсон
20
Ну, по крайней мере, нужно ;после.
Hot Licks
3
10 голосов против и не так много в комментариях, чтобы их объяснить, что очень обескураживает. Это первый вопрос ОП, и если люди считают, что это стоит 10 отрицательных голосов, должны быть некоторые объяснения. Это правильный вопрос с неочевидными последствиями, и многие интересные вещи можно узнать из ответов и комментариев.
Шафик Ягмур
3
@HotLicks - это не о плохом или хорошем самочувствии, конечно, это может показаться несправедливым, но это жизнь, в конце концов, это очки единорога. Дау-голоса, безусловно, не должны отменять голоса за, которые вам не нравятся, точно так же, как положительные голоса не отменяют отрицательные голоса, которые вам не нравятся. Если люди считают, что вопрос можно улучшить, конечно, тот, кто впервые задает вопрос, должен получить обратную связь. Я не вижу причин для отрицательного голоса, но я хотел бы знать, почему это делают другие, хотя они могут не говорить этого.
Шафик Ягмур
Ответы:
159
Заявление об ошибке не является ошибкой float a = 3.0: если вы это сделаете, компилятор преобразует за вас двойной литерал 3.0 в число с плавающей запятой.
Однако в определенных сценариях следует использовать нотацию литералов с плавающей запятой.
По соображениям производительности:
В частности, рассмотрите:
floatfoo(float x){ return x * 0.42; }
Здесь компилятор произведет преобразование (которое вы заплатите во время выполнения) для каждого возвращаемого значения. Чтобы этого избежать, вам следует заявить:
floatfoo(float x){ return x * 0.42f; } // OK, no conversion required
Чтобы избежать ошибок при сравнении результатов:
например, следующее сравнение не удается:
float x = 4.2;
if (x == 4.2)
std::cout << "oops"; // Not executed!
Мы можем исправить это с помощью буквального обозначения float:
В пункте 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 floatfloat b = 3.0; // converted to floatfloat c = 3.0f; // float
Будет иметь значение, если вы использовали auto(или другие методы вычитания), например:
auto d = 3; // intauto e = 3.0; // doubleauto f = 3.0f; // float
Типы также выводятся при использовании шаблонов, поэтому autoэто не единственный случай.
Шафик Ягмур
14
Литералы с плавающей запятой без суффикса относятся к типу double , это описано в черновом разделе стандарта C ++ 2.14.4Плавающие литералы :
[...] Тип плавающего литерала - double, если явно не указан суффикс. [...]
так что это ошибка , чтобы назначить 3.0на двойной Литерал к поплавку :
float a = 3.0
Нет, он будет преобразован, что описано в разделе 4.8Преобразования с плавающей запятой :
Prvalue типа с плавающей запятой может быть преобразовано в prvalue другого типа с плавающей запятой. Если исходное значение может быть точно представлено в целевом типе, результатом преобразования будет это точное представление. Если исходное значение находится между двумя соседними значениями назначения, результатом преобразования является выбор любого из этих значений, определяемый реализацией. В противном случае поведение не определено.
Это означает, что двойная константа может быть неявно (т. Е. Незаметно) преобразована в константу с плавающей запятой, даже если при этом теряется точность (т. Е. Данные). Это было разрешено оставить из соображений совместимости с C и удобства использования, но об этом стоит помнить при работе с плавающей запятой.
Качественный компилятор предупредит вас, если вы попытаетесь сделать что-то неопределенное, а именно поместить двойное количество в число с плавающей запятой, которое меньше минимального или больше максимального значения, которое может представлять число с плавающей запятой. Действительно хороший компилятор предоставит необязательное предупреждение, если вы попытаетесь сделать что-то, что может быть определено, но может потерять информацию, а именно поместить двойное количество в число с плавающей запятой, которое находится между минимальным и максимальным значениями, представляемыми с помощью числа с плавающей точкой, но которое не может быть представленным в точности как поплавок.
Так что есть предостережения для общего случая, о которых вам следует знать.
С практической точки зрения, в этом случае результаты, скорее всего, будут такими же, даже если технически есть преобразование, мы можем убедиться в этом, попробовав следующий код на Godbolt :
и мы видим, что результаты для func1и func2идентичны, используя оба clangи gcc:
func1():
movss xmm0, DWORD PTR .LC0[rip]
ret
func2():
movss xmm0, DWORD PTR .LC0[rip]
ret
Как указывает Паскаль в этом комментарии, вы не всегда сможете на это рассчитывать. Использование 0.1и 0.1fсоответственно приводит к тому, что сгенерированная сборка будет отличаться, поскольку преобразование теперь должно выполняться явно. Следующий код:
floatfunc1(float x ){
return x*0.1; // a double literal
}
floatfunc2(float x){
return x*0.1f ; // a float literal
}
Независимо от того, сможете ли вы определить, повлияет ли преобразование на производительность или нет, использование правильного типа лучше документирует ваше намерение. Использование явного преобразования, например, 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.
supercat
@supercat спасибо за хороший пример, я процитировал вас напрямую, пожалуйста, не стесняйтесь редактировать, как считаете нужным
Шафик Ягмур
4
Это не ошибка в том смысле, что компилятор отклонит ее, но это ошибка в том смысле, что это может быть не то, что вам нужно.
Как правильно сказано в вашей книге, 3.0это значение типа double. Существует неявное преобразование из doubleв float, так float a = 3.0;что это допустимое определение переменной.
Однако, по крайней мере концептуально, это выполняет ненужное преобразование. В зависимости от компилятора преобразование может выполняться во время компиляции или может быть сохранено во время выполнения. Веской причиной для сохранения его во время выполнения является то, что преобразования с плавающей запятой сложны и могут иметь неожиданные побочные эффекты, если значение не может быть представлено точно, и не всегда легко проверить, может ли значение быть представлено точно.
3.0f позволяет избежать этой проблемы: хотя технически компилятору все еще разрешено вычислять константу во время выполнения (это всегда так), здесь нет абсолютно никаких причин, по которым какой-либо компилятор мог бы это сделать.
Действительно, в случае кросс-компилятора было бы совершенно некорректно выполнять преобразование во время компиляции, потому что оно происходило бы на неправильной платформе.
Маркиз Лорн,
2
Хотя это не ошибка, по сути, это немного небрежно. Вы знаете, что вам нужен float, поэтому инициализируйте его с помощью float. Другой программист может прийти и не быть уверенным, какая часть объявления верна, тип или инициализатор. Почему они оба не верны?
float Answer = 42.0f;
Когда вы определяете переменную, она инициализируется предоставленным инициализатором. Для этого может потребоваться преобразование значения инициализатора в тип инициализируемой переменной. Вот что происходит, когда вы говорите float a = 3.0;: значение инициализатора преобразуется в float, а результат преобразования становится начальным значением a.
В целом это нормально, но не помешает написать, 3.0fчтобы показать, что вы осведомлены о том, что делаете, и особенно если вы хотите писать auto a = 3.0f.
что показывает, что размер 3.2f принимается как 4 байта на 32-битной машине, а 3.2 интерпретируется как двойное значение, занимающее 8 байтов на 32-битной машине. Это должно дать ответ, который вы ищете.
Это показывает, что doubleи floatони разные, но не отвечает, можно ли инициализировать floata двойным литералом
Джонатан Уэйкли
конечно, вы можете инициализировать число с плавающей запятой из двойного значения с усечением данных, если это применимо
Д-р Дебасиш Яна
4
Да, я знаю, но это был вопрос ОП, так что ваш ответ на самом деле не отвечал на него, несмотря на то, что я заявлял, что дает ответ!
Джонатан Уэйкли
0
Компилятор выводит наиболее подходящий тип из литералов или, по крайней мере, из того, что он считает наиболее подходящим. Это скорее потеря эффективности по сравнению с точностью, то есть использование double вместо float. Если есть сомнения, используйте фигурные скобки, чтобы сделать это явным:
auto d = double{3}; // make a doubleauto f = float{3}; // make a floatauto 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
.;
после.Ответы:
Заявление об ошибке не является ошибкой
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
Плавающие литералы :так что это ошибка , чтобы назначить
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 = f * 0.1;
иf = f * 0.1f;
делать разные вещи . Например, еслиf
было равно 100000224 (что в точности можно представить как afloat
), умножение его на одну десятую должно дать результат, который округляется до 10000022, но умножение на 0,1f вместо этого даст результат, который ошибочно округляется до 10000023. Если намерение состоит в том, чтобы разделить на десять, умножение наdouble
константу 0,1, вероятно, будет быстрее, чем деление на10f
, и более точным, чем умножение на0.1f
.Это не ошибка в том смысле, что компилятор отклонит ее, но это ошибка в том смысле, что это может быть не то, что вам нужно.
Как правильно сказано в вашей книге,
3.0
это значение типаdouble
. Существует неявное преобразование изdouble
вfloat
, такfloat a = 3.0;
что это допустимое определение переменной.Однако, по крайней мере концептуально, это выполняет ненужное преобразование. В зависимости от компилятора преобразование может выполняться во время компиляции или может быть сохранено во время выполнения. Веской причиной для сохранения его во время выполнения является то, что преобразования с плавающей запятой сложны и могут иметь неожиданные побочные эффекты, если значение не может быть представлено точно, и не всегда легко проверить, может ли значение быть представлено точно.
3.0f
позволяет избежать этой проблемы: хотя технически компилятору все еще разрешено вычислять константу во время выполнения (это всегда так), здесь нет абсолютно никаких причин, по которым какой-либо компилятор мог бы это сделать.источник
Хотя это не ошибка, по сути, это немного небрежно. Вы знаете, что вам нужен float, поэтому инициализируйте его с помощью float.
Другой программист может прийти и не быть уверенным, какая часть объявления верна, тип или инициализатор. Почему они оба не верны?
float Answer = 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
они разные, но не отвечает, можно ли инициализироватьfloat
a двойным литераломКомпилятор выводит наиболее подходящий тип из литералов или, по крайней мере, из того, что он считает наиболее подходящим. Это скорее потеря эффективности по сравнению с точностью, то есть использование 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'
источник