Почему C++ продвигает int в float, когда float не может представлять все значения int?
скажем, у меня есть следующие:
int i = 23;
float f = 3.14;
if (i == f) // do something
i
будет повышен до float
и два float
числа будут сравниваться, но может a float
представляют все int
значения? Почему бы не продвигать оба int
и float
до double
?
9 ответов:
, когда
int
превращается вunsigned
в интегральных акциях также теряются отрицательные значения (что приводит к такой забаве, как0u < -1
быть верным).как и большинство механизмов в C (которые наследуются в C++), обычные арифметические преобразования следует понимать с точки зрения аппаратных операций. Создатели C были очень знакомы с языком сборки машин, с которыми они работали, и они написали C, чтобы иметь непосредственный смысл для себя и таких людей, как сами при написании вещей, которые до этого были бы написаны в сборке (например, ядро UNIX).
теперь процессоры, как правило, не имеют инструкций смешанного типа (добавить float в double, сравнить int с float и т. д.) потому что это была бы огромная трата недвижимости на вафлю-вам пришлось бы реализовать столько раз больше кодов операций, сколько вы хотите поддерживать разные типы. Что у вас есть только инструкции для "добавить инт на Инт" "сравнить поплавка, чтобы поплавок", "умножение без знака с неподписанным " и др. в первую очередь необходимы обычные арифметические преобразования-они представляют собой отображение двух типов в семейство инструкций, которое имеет наибольший смысл использовать с ними.
С точки зрения того, кто привык писать машинный код низкого уровня, если у вас смешанные типы, инструкции ассемблера, которые вы, скорее всего, рассмотрите в общем случае,-это те, которые требуют наименьших преобразований. Это особенно относится к плавающим точкам, где преобразования являются дорогостоящими во время выполнения, и особенно в начале 1970-х годов, когда C был разработан, компьютеры были медленными, и когда вычисления с плавающей запятой выполнялись в программном обеспечении. Это показывает в обычных арифметических преобразованиях - только один операнд когда-либо преобразуется (с единственным исключением
long
/unsigned int
, гдеlong
может быть преобразован вunsigned long
, что не требует ничего делать на большинстве машин. Возможно, не на любом, где применяется исключение).так, обычные арифметические преобразования написаны так, чтобы делать то, что кодер сборки будет делать большую часть времени: у вас есть два типа, которые не подходят, преобразуйте один в другой, чтобы он это делал. Это то, что вы бы сделали в ассемблерном коде, если бы у вас не было конкретной причины делать иначе, и для людей, которые привыкли писать ассемблерный код и do есть конкретная причина, чтобы заставить другое преобразование, явно запрашивая, что преобразование является естественным. В конце концов, вы можете просто пиши
if((double) i < (double) f)
интересно отметить в этом контексте, кстати, что
unsigned
выше в иерархии, чемint
, Так что сравниватьint
Сunsigned
закончится беззнаковым сравнением (отсюда0u < -1
- чуть с самого начала). Я подозреваю, что это показатель, который люди в старину считалиunsigned
меньше как ограничение наint
чем в качестве расширения его диапазона значений: нам не нужен знак прямо сейчас, поэтому давайте использовать дополнительный бит для большего диапазон значений. Вы бы использовали его, если бы у вас были основания ожидать, чтоint
переполнение -- гораздо большее беспокойство в мире 16-битint
s.
даже
double
не может быть в состоянии представить всеint
значения, в зависимости от того, сколько битint
содержать.почему бы не повысить как int, так и float до double?
вероятно, потому, что это дороже конвертировать оба типа в
double
чем использовать один из операндов, который ужеfloat
, аfloat
. Он также вводит специальные правила для операторов сравнения, несовместимые с правилами для арифметических операторов.также нет гарантии, как будут представлены типы с плавающей запятой, поэтому было бы слепым выстрелом предположить, что преобразование
int
todouble
(или дажеlong double
) для сравнения решим все что угодно.
правила продвижения типа разработаны, чтобы быть простыми и работать предсказуемым образом. Типы В C / C++ естественно "сортируются" по диапазон значений они могут представлять. Смотрите этой для сведения. Хотя типы с плавающей запятой не могут представлять все целые числа, представленные целыми типами, поскольку они не могут представлять одно и то же количество значащих цифр, они могут представлять более широкий диапазон.
иметь предсказуемое поведение, при необходимости продвижения типа числовые типы всегда преобразуются в тип с больше диапазон, чтобы избежать переполнения в меньшей. Представьте себе:
int i = 23464364; // more digits than float can represent! float f = 123.4212E36f; // larger range than int can represent! if (i == f) { /* do something */ }
если преобразование было сделано в сторону интегрального типа, то float
f
несомненно, переполнение при преобразовании в int, что приводит к неопределенному поведению. С другой стороны, преобразованиеi
доf
только вызывает потерю точности, которая не имеет значения, так какf
имеет такую же точность так что все еще возможно, что сравнение удастся. В этот момент программист должен интерпретировать результат сравнения в соответствии с требованиями приложения.наконец, помимо того, что числа с плавающей запятой двойной точности страдают от одной и той же проблемы, представляющей целые числа (ограниченное количество значащих цифр), использование продвижения по обоим типам приведет к более высокоточному представлению для
i
, в то время какf
обречен иметь оригинальная точность, поэтому сравнение не будет успешным, еслиi
имеет более значительные цифры, чемf
для начала. Теперь это также неопределенное поведение: сравнение может быть успешным для некоторых пар (i
,f
), но не для других.
может a
float
представляют всеint
значения?для типичной современной системы, где обе
int
иfloat
хранятся в 32 битах, нет. Что-то должно дать. 32-битные целые числа не сопоставляются 1-к-1 на набор одинакового размера, который включает дроби.The
i
будет повышен доfloat
и дваfloat
числа будут сравниваться...не обязательно. Вы действительно не знаете, что точность будет применяться. C++14 §5/12:
значения плавающих операндов и результаты плавающих выражений могут быть представлены с большей точностью и диапазоном, чем это требуется типом; при этом типы не изменяются.
хотя
i
после промотирования имеет номинальный типfloat
значение может быть представлено с помощьюdouble
оборудование. C++ не гарантирует потери точности с плавающей запятой или переполнения. (Это не ново в C++14; он унаследован от C с давних времен.)почему бы не продвигать оба
int
иfloat
доdouble
?если вы хотите оптимальную точность везде, используйте
double
вместо этого, и вы никогда не увидитеfloat
. Илиlong double
, но это может работать медленнее. Правила разработаны, чтобы быть относительно разумными для большинства случаев использования типов ограниченной точности, учитывая, что одна машина может предложить несколько альтернативных вариантов точности.большую часть времени, быстро и свободно достаточно хорошо, так что машина может делать все, что проще всего. Это может означать округление, сравнение с одной точностью или двойную точность и отсутствие округления.
но, такие правила в конечном счете компромиссы, и иногда они терпят неудачу. Чтобы точно указать арифметику в C++ (или C), это помогает сделать конверсии и рекламные акции явными. Многие руководства по стилю для сверхнадежного программного обеспечения запрещают использование неявных преобразований в целом, и большинство компиляторов предлагают предупреждения, чтобы помочь вам удалить их.
чтобы узнать о том, как эти компромиссы пришли, вы можете прочитать C обоснование документа. (Последнее издание охватывает до C99.) Это не просто бессмысленный багаж со времен PDP-11 или K&R.
интересно, что ряд ответов здесь спорят о происхождении языка C, явно называя K&R и исторический багаж как причину того, что int преобразуется в float при объединении с float.
это указывает на вину не тех сторон. В K&R C не было такой вещи, как вычисление поплавка. все операции с плавающей запятой были выполнены с двойной точностью. По этой причине целое число (или что-либо еще) никогда не было неявно преобразуется в поплавок, но только в двойной. Float также не может быть типом аргумента функции: вам нужно было передать указатель на float, если вы действительно, действительно, действительно хотели избежать преобразования в double. По этой причине функции
int x(float a) { ... }
и
int y(a) float a; { ... }
имеют различные соглашения о вызовах. Первый получает аргумент float, второй (теперь уже не разрешимый как синтаксис) получает двойной аргумент.
одинарной точности с плавающей точкой арифметические и функциональные аргументы были введены только с ANSI C. Керниган / Ричи невиновен.
теперь с новым доступным одним поплавком выражения (single float ранее был только форматом хранения), также должны были быть преобразования нового типа. Независимо от того, что команда ANSI C выбрала здесь (и я был бы в недоумении для лучшего выбора), это не вина K & R.
Q1: может ли поплавок представлять все значения int?
IEE754 может представлять все целые числа точно так же, как плавает, до примерно 223, как уже упоминалось в этом ответ.
К2: почему не повысить и Инт и поплавок к двойнику?
правила в стандарте для этих преобразований являются незначительными изменениями тех, что в K&R: изменения учитывают добавленные типы и правила сохранения значения. явная лицензия была добавлена для выполнения вычислений в" более широком " типе, чем абсолютно необходимо, так как это может иногда производить меньший и более быстрый код, не говоря уже о правильном ответе чаще. вычисления также могут быть выполнены в "более узком" типе по правилу as if, если получен тот же конечный результат. Явное приведение всегда можно использовать для получения значения в желаемом типе.
выполнение вычисления в более широком типе означает, что данный
float f1;
иfloat f2;
,f1 + f2
может быть вычислена вdouble
точности. А это значит, что данныйint i;
иfloat f;
,i == f
может быть вычислена вdouble
точности. Но это не требуется, чтобы вычислитьi == f
в двойной точности, как hvd заявил в комментарии.также стандарт C говорит так. Они известны как обычные арифметические преобразования . Следующее описание взято прямо из ANSI C норматив.
...если один из операндов имеет тип float, другой операнд преобразуется в тип float .
источник и вы можете увидеть его в ref тоже.
соответствующая ссылка, это ответ. Более аналитическим источником является здесь.
вот еще один способ объяснить это: обычные арифметические преобразования неявно выполняются для приведения их значений в общей тип. Компилятор сначала выполняет целочисленное продвижение, если операнды все еще имеют разные типы, то они преобразуются в тип, который отображается выше в следующей иерархии:
при создании языка программирования некоторые решения принимаются интуитивно.
например, почему бы не преобразовать int + float в int+int вместо float+float или double+double? Зачем вызывать int - >float продвижение, если оно содержит то же самое о битах? Почему бы не вызвать float - >int a promotion?
Если вы полагаетесь на неявные преобразования типов, вы должны знать, как они работают, иначе просто конвертируйте вручную.
некоторые языки могли быть разработаны без каких-либо автоматическое преобразование типов вообще. И не каждое решение на этапе проектирования могло быть принято логически с хорошей причиной.
JavaScript с его утиной печатью имеет еще более неясные решения под капотом. Проектирование абсолютно логического языка невозможно, я думаю, что он идет к теореме Геделя о неполноте. Вы должны сбалансировать логику, интуицию, практику и идеалы.
вопрос в том, почему: потому что это быстро, легко объяснить, легко скомпилировать, и все это были очень важные причины в то время, когда был разработан язык C.
вы могли бы иметь другое правило: что для каждого сравнения арифметических значений результатом является сравнение фактических числовых значений. Это было бы где-то между тривиальным, если одно из сравниваемых выражений является константой, одна дополнительная инструкция при сравнении signed и unsigned int, и довольно сложно, если вы сравниваете long long и double и хотите получить правильные результаты, когда long long не может быть представлен как double. (0u
в Swift проблема легко решается путем запрещения операций между различными типами.
правила написаны для 16-битных ints (наименьший требуемый размер). Ваш компилятор с 32-битными входами наверняка преобразует обе стороны в double. В современном оборудовании нет плавающих регистров, так что это и для преобразования в double. Теперь, если у вас есть 64 бит ints, я не слишком уверен, что он делает. длинный двойной был бы уместен (обычно 80 бит, но это даже не стандарт).