Нет полезного и надежного способа обнаружения целочисленного переполнения в C / C ++? - PullRequest
11 голосов
/ 28 июля 2011

Нет, это не дубликат Как обнаружить целочисленное переполнение? . Вопрос тот же, но вопрос другой.


Компилятор gcc может оптимизировать проверку переполнения (с -O2), например:

int a, b;
b = abs(a);                     // will overflow if a = 0x80000000
if (b < 0) printf("overflow");  // optimized away

Люди из gcc утверждают, что это не ошибка. Переполнение - неопределенное поведение, согласно стандарту C, которое позволяет компилятору делать что угодно . Очевидно, что что-нибудь включает в себя предположение, что переполнение никогда не происходит. К сожалению, это позволяет компилятору оптимизировать проверку переполнения.

Безопасный способ проверки на переполнение описан в недавней статье CERT . В этой статье рекомендуется сделать что-то подобное перед добавлением двух целых чисел:

if ( ((si1^si2) | (((si1^(~(si1^si2) & INT_MIN)) + si2)^si2)) >= 0) { 
  /* handle error condition */
} else {
  sum = si1 + si2;
}

Очевидно, что вы должны делать что-то подобное перед каждыми +, -, *, / и другими операциями в серии вычислений, когда хотите убедиться, что результат верен. Например, если вы хотите убедиться, что индекс массива не выходит за пределы. Это так громоздко, что практически никто этим не занимается. По крайней мере, я никогда не видел программы на C / C ++, которая делает это систематически.

Теперь это фундаментальная проблема:

  • Проверка индекса массива перед доступом к массиву полезна, но не надежна.

  • Проверка каждой операции в серии вычислений методом CERT надежна, но бесполезна.

  • Вывод: в C / C ++ нет полезного и надежного способа проверки переполнения!

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

Я знаю, что есть определенные параметры командной строки, которые могут решить проблему, но это не меняет того факта, что у нас есть фундаментальная проблема со стандартом или его текущей интерпретацией.

Теперь мой вопрос: Неужели люди из gcc слишком интерпретируют «неопределенное поведение», когда оно позволяет оптимизировать проверку переполнения, или стандарт C / C ++ нарушен?

Добавлено примечание: Извините, вы, возможно, неправильно поняли мой вопрос. Я не спрашиваю, как обойти проблему - на это уже был дан ответ в другом месте . Я задаю более фундаментальный вопрос о стандарте C. Если нет полезного и надежного способа проверки на переполнение, то сам язык сомнительный. Например, если я создаю безопасный класс массива с проверкой границ, я должен быть в безопасности, но я не уверен, что проверка границ может быть оптимизирована.

Если стандарт допускает это, то либо стандарт требует пересмотра, либо интерпретация стандарта требует пересмотра.

Добавлено примечание 2: Люди здесь не хотят обсуждать сомнительную концепцию «неопределенного поведения». Тот факт, что стандарт C99 перечисляет 191 различное неопределенное поведение ( ссылка ), указывает на небрежный стандарт.

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

Почему эти два вида "неопределенного поведения" различны? Потому что:

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

  • Запись внешних границ массива на самом деле может сделать что-то такое же плохое, как форматирование жесткого диска (по крайней мере, в незащищенной ОС, такой как DOS), и большинство программистов знают, что это опасно.

  • Когда вы помещаете целочисленное переполнение в опасную категорию "все идет", это позволяет компилятору делать что угодно, включая ложь о том, что он делает (в случае, когда проверка на переполнение оптимизирована)

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

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

Может быть, это не подходящее место для обсуждения таких философских вопросов. По крайней мере, большинство ответов здесь не по теме. Есть ли лучшее место, чтобы обсудить это?

Ответы [ 4 ]

4 голосов
/ 28 июля 2011
int a, b;
b = abs(a); // will overflow if a = 0x80000000
if (b < 0) printf("overflow");  // optimized away 

(Похоже, вы предполагаете, что дополнение 2s ... давайте поработаем с этим)

Кто скажет abs(a) "переполнение", если a имеет этот двоичный шаблон (точнее, если a это INT_MIN)?Справочная страница Linux для abs(int) говорит:

Попытка получить абсолютное значение наиболее отрицательного целого числа не определена.

Не определено не обязательно означаетПереполнение.

Итак, ваша предпосылка, что b может быть меньше 0, и это каким-то образом является проверкой на «переполнение», в корне ошибочна с самого начала.Если вы хотите протестировать, вы не можете сделать это с результатом, который может иметь неопределенное поведение - вместо этого сделайте это перед операцией!

Если вы заботитесь об этом, вы можете использовать пользовательские типы C ++ (то есть классы)) реализовать свой собственный набор тестов вокруг необходимых вам операций (или найти библиотеку, которая уже делает это).Язык не нуждается во встроенной поддержке для этого, поскольку он может быть одинаково эффективно реализован в такой библиотеке, с результирующей семантикой использования без изменений.Это фундаментальная сила - одна из замечательных особенностей C ++.

4 голосов
/ 28 июля 2011

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

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

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

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

4 голосов
/ 28 июля 2011

Спросите себя: как часто вам нужна проверенная арифметика?Если вам это нужно часто, вы должны написать класс checked_int, который перегружает общие операторы и инкапсулирует проверки в этот класс.Рекомендует поделиться реализацией на веб-сайте с открытым исходным кодом.

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

2 голосов
/ 28 июля 2011

Просто используйте правильный тип для b:

int a;
unsigned b = a;
if (b == (unsigned)INT_MIN) printf("overflow");  // never optimized away
else b = abs(a);

Редактировать: Проверка на переполнение в C может быть безопасно выполнена с беззнаковым типом. Неподписанные типы просто переносятся на арифметику, и подписанные типы безопасно преобразуются в них. Таким образом, вы можете сделать любой тест на них, который вам нравится. На современных процессорах это преобразование обычно представляет собой просто реинтерпретацию регистра или около того, поэтому оно не требует затрат времени выполнения.

...