Предупреждение C ++: деление двойного на ноль

98

Случай 1:

#include <iostream>

int main()
{
    double d = 15.50;
    std::cout<<(d/0.0)<<std::endl;
}

Компилируется без предупреждений и выводов inf. Хорошо, C ++ может обрабатывать деление на ноль ( посмотреть вживую ).

Но,

Случай 2:

#include <iostream>

int main()
{
    double d = 15.50;
    std::cout<<(d/0)<<std::endl;
}

Компилятор выдает следующее предупреждение ( посмотрите его вживую ):

warning: division by zero [-Wdiv-by-zero]
     std::cout<<(d/0)<<std::endl;

Почему компилятор выдает предупреждение во втором случае?

Есть 0 != 0.0?

Редактировать:

#include <iostream>

int main()
{
    if(0 == 0.0)
        std::cout<<"Same"<<std::endl;
    else
        std::cout<<"Not same"<<std::endl;
}

вывод:

Same
Джайеш
источник
9
Я предполагаю, что во втором случае он принимает ноль как целое число и отбрасывает предупреждение, даже если вычисление будет выполнено позже с использованием double (что, я думаю, должно быть поведением, когда d является двойным).
Qubit
10
На самом деле это проблема QoI. Ни предупреждение, ни его отсутствие не предусмотрены самим стандартом C ++. Вы используете GCC?
StoryTeller - Unslander Моника
5
@StoryTeller Что такое QoI? en.wikipedia.org/wiki/QoI ?
user202729
5
Что касается вашего последнего вопроса, «0 - это то же самое, что и 0,0?» Ответ заключается в том, что значения одинаковы, но, как вы выяснили, это не означает, что они идентичны. Различные виды! Точно так же, как «А» не идентично 65.
Мистер Листер,

Ответы:

108

Деление с плавающей запятой на ноль хорошо определено IEEE и дает бесконечность (положительную или отрицательную в зависимости от значения числителя (или NaN± 0) ).

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

Однако в этом случае, поскольку числитель - это double, divisor ( 0) также должен быть повышен до двойного, и нет причин давать здесь предупреждение, не давая предупреждения, 0.0поэтому я думаю, что это ошибка компилятора.

Мотти
источник
8
Однако оба являются делениями с плавающей запятой. В d/0, 0преобразуется к типу d.
43
Обратите внимание, что C ++ не требуется для использования IEEE 754 (хотя я никогда не видел компилятора, использующего другой стандарт) ..
Yksisarvinen
1
@hvd, хороший момент, в этом случае это похоже на ошибку компилятора
Мотти
14
Я согласен с тем, что он должен либо предупреждать в обоих случаях, либо не предупреждать в обоих случаях (в зависимости от того, как компилятор обрабатывает плавающее деление на ноль)
MM
8
Совершенно уверен, что деление с плавающей запятой на ноль тоже UB - просто GCC реализует его в соответствии с IEEE 754. Но им это не обязательно.
Мартин Боннер поддерживает Монику
42

В стандартном C ++ оба случая являются неопределенным поведением . Может случиться что угодно, включая форматирование жесткого диска. Вы не должны ожидать или полагаться на «return inf. Ok» или любое другое поведение.

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

Из стандарта C ++ 17 [expr.mul] / 4:

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

ММ
источник
21
Неправда, в арифметике с плавающей запятой деление на ноль хорошо определено.
Motti
9
@Motti - Если ограничиться одним стандартом C ++, таких гарантий нет. Честно говоря, объем этого вопроса не уточняется.
StoryTeller - Unslander Моника
9
@StoryTeller Я почти уверен (хотя я не смотрел сам стандартный документ для этого), что если std::numeric_limits<T>::is_iec559есть true, то деление на ноль для Tне UB (и на большинстве платформ оно trueдля doubleи float, хотя для переносимости вы необходимо явно проверить это с помощью ifили if constexpr).
Daniel H
6
@DanielH - «Разумно» на самом деле довольно субъективно. Если бы этот вопрос был помечен как « язык-юрист», то был бы совсем другой (гораздо меньший) набор разумных предположений.
StoryTeller - Unslander Моника
5
@MM Помните, что это именно то, что вы сказали, но не более того: undefined не означает, что никаких требований не налагается, это означает, что стандарт не налагает никаких требований . Я бы сказал, что в этом случае тот факт, что реализация определяет is_iec559как trueозначает, что реализация документирует поведение, которое стандарт оставляет неопределенным. Просто это тот случай, когда документацию по реализации можно читать программно. Даже не единственный: то же самое относится и к is_moduloцелочисленным типам со знаком.
12

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

Итак, шаги будут такими:

  1. Анализировать выражение
  2. Арифметика оператор /(T, T2) , где T=double, T2=int.
  3. Проверьте, что std::is_integral<T2>::valueесть trueи b == 0- это вызывает предупреждение.
  4. Создать предупреждение
  5. Выполнить неявное преобразование T2вdouble
  6. Выполните четко определенное деление (поскольку компилятор решил использовать IEEE 754).

Это, конечно, предположение и основано на спецификациях, определенных компилятором. Со стандартной точки зрения, мы имеем дело с возможным неопределенным поведением.


Обратите внимание, что это ожидаемое поведение в соответствии с документацией GCC
(кстати, похоже, что этот флаг нельзя использовать явно в GCC 8.1)

-Wdiv-by-zero
Предупредить о целочисленном делении на ноль во время компиляции. Это по умолчанию. Чтобы запретить отображение предупреждающих сообщений, используйте -Wno-div-by-zero. О делении с плавающей запятой на ноль не предупреждают, так как это может быть законным способом получения бесконечностей и NaN.

Yksisarvinen
источник
2
Компиляторы C ++ работают не так. Компилятор должен выполнить разрешение перегрузки, /чтобы знать, что это деление. Если бы левая сторона была Fooобъектом, а там был бы operator/(Foo, int), то это могло бы даже не быть деление. Компилятор знает это деление только тогда, когда он выбрал built-in / (double, double)неявное преобразование правой части. Но это означает, что он НЕ выполняет деление на int(0), он выполняет деление на double(0).
MSalters
@MSalters Пожалуйста, посмотрите это. Мои знания о C ++ ограничены, но по ссылке operator /(double, int), безусловно, приемлемы. Затем он говорит, что преобразование выполняется до любого другого действия, но GCC может сжать быструю проверку, T2является ли тип целочисленным, b == 0и выдать предупреждение, если это так. Не уверен, что это полностью соответствует стандарту, но компиляторы имеют полную свободу в определении предупреждений и времени их запуска.
Yksisarvinen
2
Здесь мы говорим о встроенном операторе. Это забавно. На самом деле это не функция, поэтому вы не можете узнать ее адрес. Следовательно, вы не можете определить, operator/(double,int)существует ли на самом деле. Например, компилятор может решить оптимизировать a/bдля константы b, заменив ее на a * (1/b). Конечно, это означает, что вы больше не звоните operator/(double,double)во время выполнения, а быстрее operator*(double,double). Но теперь отключается оптимизатор 1/0, константа, которой он должен был бы питатьсяoperator*
MSalters
@MSalters Обычно деление с плавающей запятой нельзя заменить умножением, вероятно, за исключением исключительных случаев, таких как 2.
user202729
2
@ user202729: GCC делает это даже для целочисленного деления. Позвольте этому погрузиться на мгновение. GCC заменяет целочисленное деление целочисленным умножением. Да, это возможно, потому что GCC знает, что работает с кольцом (числа по модулю 2 ^ N)
MSalters
9

В этом ответе я не буду вдаваться в разгром UB / not UB.

Я просто хочу указать на это, 0и 0.0 они разные, несмотря на то, что их0 == 0.0 оценка истинна. 0является intбуквальным и 0.0является doubleбуквальным.

Однако в этом случае конечный результат тот же: d/0деление с плавающей запятой, потому что dоно double и поэтому 0неявно преобразуется в double.

болов
источник
5
Я не понимаю, насколько это актуально, учитывая, что обычные арифметические преобразования указывают, что деление a doubleна intсредство, в intкоторое конвертируется double , и это указано в стандарте, который 0преобразуется в 0.0 (conv.fpint / 2)
MM,
@MM OP хочет знать, 0то же ли это, что и0.0
bolov
2
На вопрос написано «Есть 0 != 0.0?». OP никогда не спрашивает, «одинаковы ли они». Также мне кажется, что цель вопроса состоит в том, d/0можно ли вести себя иначе, чемd/0.0
MM
2
@MM - ОП спрашивал . Они действительно не демонстрируют приличного сетевого этикета с этими постоянными правками.
StoryTeller - Unslander Моника
7

Я считаю , что foo/0и foo/0.0это не то же самое. А именно, результирующий эффект первого (целочисленное деление или деление с плавающей запятой) сильно зависит от типа foo, в то время как то же самое не верно для второго (это всегда будет деление с плавающей запятой).

Не имеет значения, является ли какой-либо из двух UB. Цитата из стандарта:

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

(Акцент мой)

Обратите внимание на предупреждение « предлагать круглые скобки вокруг присваивания, используемого в качестве истинного значения »: способ сообщить компилятору, что вы действительно хотите использовать результат присваивания, явиться явным и добавить круглые скобки вокруг присваивания. Результирующий оператор имеет тот же эффект, но сообщает компилятору, что вы знаете, что делаете. То же самое можно сказать и о foo/0.0: поскольку вы явно указываете компилятору «Это деление с плавающей запятой», используя 0.0вместо 0, компилятор доверяет вам и не выдаст предупреждение.

Кассио Ренан
источник
1
Оба должны пройти обычные арифметические преобразования, чтобы привести их к общему типу, что оставит в обоих случаях деление с плавающей запятой.
Shafik Yaghmour
@ShafikYaghmour Вы упустили суть ответа. Обратите внимание, что я никогда не упоминал, что это за тип foo. Это сделано намеренно. Ваше утверждение верно только в том случае, если fooэто тип с плавающей запятой.
Cássio Renan
Я не знал, компилятор имеет информацию о типах и понимает преобразования, возможно, текстовый статический анализатор может быть пойман такими вещами, но компилятор не должен.
Shafik Yaghmour
Я хочу сказать, что да, компилятор знает обычные арифметические преобразования, но он предпочитает не выдавать предупреждение, когда программист явно выражается. Все дело в том, что это, вероятно, не ошибка, а намеренное поведение.
Cássio Renan
Тогда документация, на которую я указал, неверна, поскольку оба случая являются делением с плавающей запятой. Значит, либо документация неверна, либо в диагностике есть ошибка.
Shafik Yaghmour
4

Это похоже на ошибку gcc, в документации -Wno-div-by-zero четко сказано :

Не предупреждать о целочисленном делении на ноль во время компиляции. О делении с плавающей запятой на ноль не предупреждают, так как это может быть законным способом получения бесконечностей и NaN.

и после обычных арифметических преобразований, описанных в [expr.arith.conv], оба операнда будут двойными :

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

...

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

и [expr.mul] :

Операнды * и / должны иметь арифметический тип или тип перечисления без области действия; операнды% должны иметь целочисленный или незаданный тип перечисления. Обычные арифметические преобразования выполняются с операндами и определяют тип результата.

Что касается того, является ли деление с плавающей запятой на ноль неопределенным поведением, и как разные реализации с этим справляются, мой ответ здесь . TL; DR; Похоже, что gcc соответствует Приложению F относительно деления с плавающей запятой на ноль, поэтому undefined здесь не играет роли. Для clang ответ был бы другим.

Шафик Ягмур
источник
2

Деление с плавающей запятой на ноль ведет себя иначе, чем целочисленное деление на ноль.

Стандарт IEEE с плавающей запятой различает + inf и -inf, в то время как целые числа не могут хранить бесконечность. Целочисленное деление на нулевой результат - неопределенное поведение. Деление с плавающей запятой на ноль определяется стандартом с плавающей запятой и приводит к + inf или -inf.

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