Почему 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 67

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-бит ints.

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

почему бы не повысить как int, так и float до double?

вероятно, потому, что это дороже конвертировать оба типа в double чем использовать один из операндов, который уже float, а float. Он также вводит специальные правила для операторов сравнения, несовместимые с правилами для арифметических операторов.

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

соответствующая ссылка, это ответ. Более аналитическим источником является здесь.

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

enter image description here

источник.

при создании языка программирования некоторые решения принимаются интуитивно.

например, почему бы не преобразовать 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 бит, но это даже не стандарт).