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

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

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

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

Ответ 1

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

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

Теперь процессоры, как правило, не имеют команд смешанного типа (добавьте float для двойного сравнения, сравните int с float и т.д.), потому что это будет огромная трата недвижимости на пластине - вы бы должны выполнять столько раз больше кодов операций, сколько хотите поддерживать разные типы. У вас есть только инструкции для "добавить int в int", "сравнить float to float", "multiply unsigned with unsigned" и т.д., Что делает обычные арифметические преобразования в первую очередь - это сопоставление двух типов с инструкцией семья, которая имеет смысл использовать с ними.

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

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

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

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

Ответ 2

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

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

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

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

Ответ 3

Правила продвижения по типу разработаны, чтобы быть простыми и работать предсказуемым образом. Типы в 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, что приведет к поведению undefined. С другой стороны, преобразование i в f приводит к потере точности, которая не имеет значения, поскольку f имеет одинаковую точность, поэтому все еще возможно, что сравнение будет успешным. В этот момент программист должен интерпретировать результат сравнения в соответствии с требованиями приложения.

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

Ответ 4

может ли float отображать все значения int?

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

i будет продвигаться до float, а два float будут сравниваться...

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

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

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

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

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

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

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

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

Ответ 5

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

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

int x(float a)
{ ... }

и

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

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

Одиночные арифметические и функциональные аргументы с плавающей запятой были введены только с ANSI C. Kernighan/Ritchie невинно.

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

Ответ 6

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

IEE754 может представлять все целые числа точно так же, как float, до около 2 23 как указано в этом .

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

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

Источник

Выполнение вычислений в более широком типе означает, что при заданных 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

Источник.

Ответ 7

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

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

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

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

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

Ответ 8

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

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

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

Ответ 9

Правила написаны для 16-битных ints (наименьший требуемый размер). Ваш компилятор с 32-битным ints превращает обе стороны в двойные. В любом случае в современном оборудовании нет плавающих регистров, поэтому он должен преобразовать в двойное. Теперь, если у вас 64-битные ints, я не слишком уверен, что он делает. длинный двойной будет подходящим (обычно 80 бит, но он даже не стандартный).