Всегда ли полезно деление на ноль, сопровождаемое ошибкой времени выполнения, в C++?

Согласно стандарту С++ (5/5) деление на ноль является неопределенным поведением. Теперь рассмотрим этот код (множество бесполезных операторов, чтобы компилятор не оптимизировал код):

int main()
{
    char buffer[1] = {};
    int len = strlen( buffer );
    if( len / 0 ) {
        rand();
    }
}

Visual C++ компилирует оператор if следующим образом:

sub         eax,edx 
cdq 
xor         ecx,ecx 
idiv        eax,ecx 
test        eax,eax 
je          wmain+2Ah (40102Ah) 
call        rand

Ясно, что компилятор видит, что код должен делить на ноль - он использует шаблон xor x,x для обнуления ecx, который затем служит вторым операндом в целочисленном делении. Этот код определенно вызовет ошибку «целочисленное деление на ноль» во время выполнения.

IMO такие случаи (когда компилятор знает, что код всегда будет делить на ноль) стоят ошибки времени компиляции - Стандарт этого не запрещает. Это помогло бы диагностировать такие случаи во время компиляции, а не во время выполнения.

Однако я поговорил с несколькими другими разработчиками, и они, кажется, не согласны — их возражение: «Что, если автор захочет разделить на ноль, чтобы… эмм… проверить обработку ошибок?»

Преднамеренное деление на ноль без ведома компилятора не так уж сложно - с помощью декоратора функций __declspec(noinline) Visual C++:

__declspec(noinline)
void divide( int what, int byWhat )
{
    if( what/byWhat ) {
       rand();
    }
}

void divideByZero()
{
    divide( 0, 0 );
}

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

Я что-то упускаю? Нужно ли разрешать выброс кода, который, как известно компилятору, делится на ноль?


person sharptooth    schedule 17.10.2011    source источник
comment
Я предполагаю, что ваш вопрос относится только к фундаментальным типам. Я прав?   -  person R. Martinho Fernandes    schedule 17.10.2011
comment
@Р. Мартиньо Фернандеш: Да, давайте считать, что это ограничивается фундаментальными типами.   -  person sharptooth    schedule 17.10.2011
comment
Проблема, которую я вижу, заключается в том, что только меньшинство ошибок будет действительно обнаружено во время компиляции, остальные - действительно ошибки времени выполнения. Я не думаю, что существует другой класс ошибок, обладающий такой двойственностью. Что немного странно.   -  person Martin York    schedule 17.10.2011
comment
Также я бы предпочел иметь предупреждение, чтобы его можно было включать/выключать по мере необходимости для обратной сопоставимости/тестирования и т. д.   -  person Martin York    schedule 17.10.2011
comment
Вы также рассматриваете деление с плавающей запятой на ноль? Если это так, это может быть очень полезно.   -  person Drew Hall    schedule 17.10.2011
comment
Думаю, меня зацепило включение ассемблера в вопрос о синтаксисе.   -  person David Heffernan    schedule 17.10.2011
comment
На самом деле, будет ли допустимо выдавать ошибку компиляции при делении на ноль? Разве неопределенное поведение не является поведением во время выполнения?   -  person David Heffernan    schedule 17.10.2011
comment
@DavidHeffernan: нет, это просто не определено. Это может даже привести к искажению времени и вызвать ошибку компиляции в коде, который вы скомпилировали вчера. Для компилятора совершенно законно выдавать ошибку при неопределенном поведении. Undefined означает именно то, что написано на коробке. Нет определения того, что он должен или не должен делать, и поэтому нет правила, согласно которому все, что он делает, должно ограничиваться временем выполнения.   -  person jalf    schedule 17.10.2011
comment
Я не был убежден, что компилятору разрешено отклонять хорошо сформированную программу только из-за существования неопределенного поведения, но, по-видимому, это возможно. 1.3, согласно определению неопределенного поведения: допустимое неопределенное поведение находится в диапазоне от . . . до прекращения трансляции... (с выдачей диагностического сообщения). Это в примечании, а не в правильном правиле, но для меня этого достаточно. Тем не менее, я все же не хотел бы, чтобы компиляторы были слишком агрессивны с этой конкретной опцией.   -  person Dennis Zickefoose    schedule 17.10.2011


Ответы (5)


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

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

person Simon Nickerson    schedule 17.10.2011
comment
Хорошо, но тогда эти функции ломаются, не так ли? - person sharptooth; 17.10.2011
comment
Последняя строка кажется мне самой важной. Если в 99% случаев компилятор не может знать, что это произойдет, сколько вы действительно выиграете в оставшихся случаях? - person Dennis Zickefoose; 17.10.2011
comment
Я думаю, что последний абзац является ответом. Компиляторы и так достаточно медленные. Стоит ли проверять еще одну ошибку, когда она почти никогда не поможет пользователю? Почти все деления на ноль зависят от ввода, так что же мы выиграем, проверив его во время компиляции? - person jalf; 17.10.2011
comment
@sharptooth: Может быть, а может и нет. Что, если причина, по которой они никогда не вызываются, является также причиной, по которой они содержат деление на ноль? Это скорее похоже на предупреждение об отсутствии оператора return... если это происходит только тогда, когда i < 0, но i < 0 означает, что какая-то функция, которую я вызываю, собирается генерировать исключение, которое я не собираюсь перехватывать, тогда функция отлично сформирована и сложна. ошибка была бы излишне агрессивной.. - person Dennis Zickefoose; 17.10.2011

Деление на 0 — это поведение undefined, поскольку на некоторых платформах оно может вызвать аппаратное исключение. Мы все могли бы желать лучшего поведения оборудования, но поскольку никто никогда не считал нужным иметь целые числа со значениями -INF/+INF и NaN, это совершенно бессмысленно.

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

int foo(char* buf, int i) {
  if (5 / i == 3) {
    return 1;
  }

  if (buf != buf + i) {
    return 2;
  }

  return 0;
}

Поскольку i используется в качестве делителя, то это не 0. Следовательно, второе if тривиально истинно и может быть оптимизировано.

Перед лицом таких преобразований любой, кто надеется на разумное поведение деления на 0..., будет жестоко разочарован.

person Matthieu M.    schedule 17.10.2011
comment
Боюсь, я не могу найти утверждение в этом фрагменте. - person sharptooth; 17.10.2011
comment
Что ж, второе утверждение if тривиально верно и может быть оптимизировано. Я предполагаю, что это то, что он имел в виду. - person jalf; 17.10.2011
comment
@sharptooth: я думаю, он имеет в виду тест if (buf!= buf + i). Поскольку я не должен быть равен нулю (или мы находимся в UB-стране), этот тест тривиально верен, и его и все, что за ним следует, можно законно заменить возвратом 2. - person Drew Hall; 17.10.2011
comment
@jalf: да, извините, я изменил пример: x - person Matthieu M.; 17.10.2011
comment
Хороший пример. Еще лучше то, что всю функцию можно просто заменить на return 2' since there is no integer value for i`, где 5/i == 3. - person edA-qa mort-ora-y; 17.10.2011

В случае целочисленных типов (int, short, long и т. д.) я не могу придумать никаких применений для преднамеренного деления на ноль навскидку.

Однако для типов с плавающей запятой на оборудовании, совместимом с IEEE, явное деление на ноль чрезвычайно полезно. Вы можете использовать его для получения положительных и отрицательных значений бесконечности (+/- 1/0), а не числовых значений (NaN, 0/0), что может быть весьма полезным.

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

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

Хотя вы можете получить доступ к этим значениям с помощью макросов и std::numeric_limits (в C++), полезно иметь возможность создавать их самостоятельно (и позволяет избежать большого количества кода "особого случая"). Это также позволяет разработчикам стандартной библиотеки не прибегать к хакерским действиям (таким как ручная сборка правильной последовательности битов FP) для предоставления этих значений.

person Drew Hall    schedule 17.10.2011

Если компилятор обнаруживает деление на 0, в ошибке компилятора нет абсолютно ничего плохого. Разработчики, с которыми вы разговаривали, ошибаются — вы можете применить эту логику к каждой отдельной ошибке компиляции. Нет смысла делить на 0.

person Ayjay    schedule 17.10.2011
comment
... или вы можете просто проголосовать за него и сказать, что отрицательные отзывы без комментариев о сносных ответах с нулевым баллом - это отстой ... +1 - person HostileFork says dont trust SE; 17.10.2011

Обнаружение деления на ноль во время компиляции — это то, что вы хотели бы иметь в качестве предупреждения компилятора. Это определенно хорошая идея.

Я не дружу с Microsoft Visual C++, но G++ 4.2.1 делает такую ​​проверку. Попробуйте скомпилировать:

#include <iostream>

int main() {
    int x = 1;
    int y = x / 0;
    std::cout << y;
    return 0;
}

И оно скажет вам:

test.cpp: In function ‘int main()’:
test.cpp:5: warning: division by zero in ‘x / 0’

Но считать это ошибкой — это скользкий путь, на который сообразительные не тратят слишком много свободного времени, лазая по нему. Подумайте, почему G++ нечего сказать, когда я пишу:

int main() {
    while (true) {
    }
    return 0;
}

Как вы думаете, он должен скомпилировать это или выдать ошибку? Должен ли он всегда выдавать предупреждение? Если вы считаете, что он должен вмешиваться во всех таких случаях, я с нетерпением жду вашей копии написанного вами компилятора, который компилирует только программы, гарантирующие успешное завершение! :-)

person HostileFork says dont trust SE    schedule 17.10.2011
comment
MSVC выдает предупреждение. warning C4723: potential divide by 0 - person Dennis Zickefoose; 17.10.2011