типы — Почему C ++ продвигает int в float, когда float не может представлять все значения int?

Скажи, что у меня есть следующее:

int i = 23;
float f = 3.14;
if (i == f) // do something

i будет повышен до float и два float числа будут сравниваться, но можно float представлять все int ценности? Почему бы не продвигать оба int и float к double?

67

Решение

когда int повышен до unsigned в интегральных акциях отрицательные значения также теряются (что приводит к такому веселью, как 0u < -1 быть правдой).

Как и большинство механизмов в C (которые наследуются в C ++), обычные арифметические преобразования следует понимать с точки зрения аппаратных операций. Создатели C были очень хорошо знакомы с языком ассемблера машин, с которыми они работали, и они написали C, чтобы иметь непосредственный смысл для себя и людей, подобных себе, при написании вещей, которые до этого были бы написаны на ассемблере (например, UNIX). ядро).

Теперь процессоры, как правило, не имеют инструкций смешанного типа (добавьте float к double, сравните int с float и т. Д.), Потому что это будет огромной тратой недвижимости на пластине — вам придется реализовать столько опкодов, сколько вы хотите, чтобы поддерживать различные типы. То, что у вас есть только инструкции для «добавить int к int», «сравнить float с float», «умножить без знака на unsigned» и т. Д., Делает обычные арифметические преобразования необходимыми в первую очередь — они представляют собой отображение двух типов на инструкцию семья, которая имеет больше всего смысла использовать с ними.

С точки зрения того, кто привык писать низкоуровневый машинный код, если у вас смешанные типы, инструкции на ассемблере, которые вы, скорее всего, рассмотрите в общем случае, — это инструкции, которые требуют наименьшего количества преобразований. Это особенно относится к плавающим точкам, где преобразования требуют больших затрат времени, особенно в начале 1970-х годов, когда разрабатывался C, компьютеры работали медленно и когда вычисления с плавающей точкой выполнялись программно. Это показано в обычных арифметических преобразованиях — только один операнд когда-либо был преобразован (за единственным исключением: long/unsigned int, где long может быть преобразован в unsigned long, что не требует ничего делать на большинстве машин. Возможно, не в любом месте, где применяется исключение).

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

if((double) i < (double) f)

Интересно отметить в этом контексте, кстати, что unsigned выше в иерархии, чем intтак что сравнивая int с unsigned закончится в неподписанном сравнении (отсюда 0u < -1 немного с самого начала). Я подозреваю, что это показатель того, что люди в старину считали unsigned меньше как ограничение на int чем как расширение его диапазона значений: нам сейчас не нужен знак, поэтому давайте используем дополнительный бит для большего диапазона значений. Вы бы использовали его, если бы у вас были основания ожидать, что int переполнится — гораздо большее беспокойство в мире 16-бит ints.

68

Другие решения

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

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

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

Также нет гарантии, как будут представлены типы с плавающей запятой, поэтому было бы ошибочным предположить, что преобразование int в double (или даже long double) для сравнения все решу.

13

Правила продвижения типов разработаны так, чтобы быть простыми и работать предсказуемым образом. Типы в 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 */ }

Если преобразование было выполнено в целочисленный тип, то число с плавающей запятой f непременно переполнится при преобразовании в int, что приведет к неопределенному поведению. С другой стороны, преобразование i в f вызывает только потерю точности, которая не имеет значения, так как f имеет ту же точность, так что все еще возможно, что сравнение успешно. На этом этапе программист должен интерпретировать результат сравнения в соответствии с требованиями приложения.

Наконец, помимо того факта, что числа с плавающей запятой двойной точности страдают от одной и той же проблемы, представляющей целые числа (ограниченное число значащих цифр), использование продвижения для обоих типов приведет к более высокоточному представлению для i, в то время как f обречен иметь оригинальную точность, поэтому сравнение не удастся, если i имеет более значимые цифры, чем f начать с. Теперь это также неопределенное поведение: сравнение может быть успешным для некоторых пар (i,f) но не для других.

10

может float представлять все int ценности?

Для типичной современной системы, где оба int а также float хранятся в 32 битах, нет. Что-то должно дать. 32-битные целые числа не отображают 1-к-1 на набор одинакового размера, который включает дроби.

i будет повышен до float и два float цифры будут сравниваться …

Не обязательно. Вы действительно не знаете, какая точность будет применяться. C ++ 14 §5 / 12:

Значения плавающих операндов и результаты плавающих выражений могут быть представлены с большей точностью и диапазоном, чем требуется типом; типы не изменяются при этом.

Хотя i после продвижения имеет номинальный тип floatзначение может быть представлено с помощью double аппаратное обеспечение. C ++ не гарантирует потерю точности с плавающей точкой или переполнение. (Это не ново в C ++ 14; оно унаследовано от C издревле.)

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

Если вам нужна оптимальная точность везде, используйте double вместо этого, и вы никогда не увидите float, Или же long double, но это может работать медленнее. Правила разработаны так, чтобы быть относительно разумными для большинства случаев использования типов с ограниченной точностью, учитывая, что одна машина может предложить несколько альтернативных значений точности.

Большую часть времени достаточно быстро и свободно, поэтому машина может делать все, что угодно. Это может означать округленное сравнение с одинарной точностью или двойную точность без округления.

Но такие правила в конечном итоге являются компромиссами, а иногда они терпят неудачу. Чтобы точно определить арифметику в C ++ (или C), это помогает сделать явные преобразования и продвижения. Многие руководства по стилю для сверхнадежного программного обеспечения вообще запрещают использование неявных преобразований, и большинство компиляторов предлагают предупреждения, чтобы помочь вам удалить их.

Чтобы узнать, как возникли эти компромиссы, вы можете просмотреть Обоснование документа. (Последнее издание охватывает до C99.) Это не просто бессмысленный багаж со времен PDP-11 или K&Р.

8

Интересно, что ряд ответов здесь вытекает из происхождения языка C, явно называя K&R и исторический багаж как причина того, что int преобразуется в число с плавающей точкой в ​​сочетании с плавающей точкой.

Это указывает на вину не на те стороны. В К&R C, не было такого понятия, как вычисление с плавающей запятой. Все Операции с плавающей запятой выполнялись с двойной точностью. По этой причине целое число (или что-либо еще) никогда не было неявно преобразовано в число с плавающей точкой, но только в двойное число. Тип float также не может быть типом аргумента функции: вы должны были передать указатель на метод float, если вы действительно хотите избежать преобразования в double. По этой причине функции

int x(float a)
{ ... }

а также

int y(a)
float a;
{ ... }

имеют разные соглашения о вызовах. Первый получает аргумент с плавающей запятой, второй (более не допустимый в качестве синтаксиса) получает двойной аргумент.

Арифметика с плавающей точкой одинарной точности и аргументы функций были введены только в ANSI C. Керниган / Ричи невиновен.

Теперь с недавно доступным одиночным поплавком выражения (одиночный float ранее был только форматом хранения), также должны были быть новые преобразования типов. Что бы команда ANSI C ни выбрала здесь (и я был бы в недоумении для лучшего выбора), это не вина K&Р.

6

Q1: может ли float представлять все значения int?

IEE754 может представлять все целые числа в точности как числа с плавающей точкой, примерно до 223, как уже упоминалось в этом ответ.

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

Правила в Стандарте для этих преобразований являются небольшими модификациями правил в K&R: модификации учитывают добавленные типы и правила сохранения значений. Явная лицензия была добавлена ​​для выполнения вычислений в «более широком» типе, чем это абсолютно необходимо, поскольку это может иногда приводить к созданию меньшего и более быстрого кода, не говоря уже о правильном ответе чаще. Расчеты также могут выполняться в «более узком» виде по правилу «как будто», если получен тот же конечный результат. Явное приведение может всегда использоваться для получения значения в желаемом типе.

Источник

Выполнение расчетов в более широком типе означает, что данный float f1; а также float f2;, f1 + f2 может быть рассчитан в double точность. А это значит что дано int i; а также float f;, i == f может быть рассчитан в double точность. Но не требуется рассчитывать i == f в двойной точности, как указано в комментарии.

Также стандарт C говорит так. Они известны как обычные арифметические преобразования. Следующее описание взято прямо из стандарта ANSI C.

…если один из операндов имеет тип float, другой операнд преобразуется в тип float.

Источник и вы можете увидеть это в ссылка тоже.

Соответствующая ссылка это ответ. Более аналитический источник Вот.

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

введите описание изображения здесь

Источник.

5

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

Например, почему бы не преобразовать int + float в int + int вместо float + float или double + double? Зачем называть int-> float повышением, если оно содержит то же самое о битах? Почему бы не назвать float-> int промоушен?

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

Некоторые языки могли бы быть разработаны без каких-либо автоматических преобразований типов вообще. И не каждое решение на этапе проектирования могло быть принято логически по уважительной причине.

У JavaScript с его утиной печатью есть еще более туманные решения под капотом. Разработка абсолютно логичного языка невозможна, я думаю, что это идет к теореме Гёделя о неполноте. Вы должны сбалансировать логику, интуицию, практику и идеалы.

3

Вопрос в том, почему: потому что это быстро, легко объяснить, легко компилировать, и все это было очень важными причинами в то время, когда разрабатывался язык Си.

Вы могли бы иметь другое правило: чтобы при каждом сравнении арифметических значений получался результат сравнения фактических числовых значений. Это было бы где-то между тривиальным, если одно из сравниваемых выражений является константой, одной дополнительной инструкцией при сравнении int со знаком и без знака, и довольно сложно, если вы сравниваете long long и double и хотите получить правильные результаты, когда long long нельзя представить как double. (0u < -1 будет ложным, потому что он будет сравнивать числовые значения 0 и -1 без учета их типов).

В Swift проблему легко решить, запретив операции между различными типами.

2
По вопросам рекламы [email protected]