Определено ли поведение или реализация целочисленного переполнения со знаком неопределенного поведения?
#include <limits.h>
int main(){
int a = UINT_MAX;
return 0;
}
Я этот УБ или реализацию определил?
Ссылки говорят, что это UB
https://www.gnu.org/software/autoconf/manual/autoconf-2.63/html_node/Integer-Overflow-Basics
Разрешение подписанных целочисленных переполнений в C / C++
Ссылки, говорящие о том, что его реализация определена
Правило преобразования гласит:
В противном случае новый тип подписывается и значение не может быть в нем представлено; либо результат определяется реализацией, либо возникает сигнал, определяемый реализацией.
Разве мы не преобразовываем
max unsigned value
в
signed value
?
Как я видел, gcc просто обрезает результат.
4 ответа
Обе ссылки верны, но они не касаются одной и той же проблемы.
int a = UINT_MAX;
не является экземпляром целочисленного переполнения со знаком, это преобразование из
unsigned int
со значением, превышающим диапазон типа
int
. Как указано на сайте École polytechnique , стандарт C определяет поведение как определенное реализацией.
#include <limits.h>
int main(){
int a = UINT_MAX; // implementation defined behavior
int b = INT_MAX + 1; // undefined behavior
return 0;
}
Вот текст из стандарта C:
6.3.1.3 Целые числа со знаком и без знака
Когда значение с целочисленным типом преобразуется в другой целочисленный тип, отличный от
_Bool
, если значение может быть представлено новым типом, оно не изменяется.В противном случае, если новый тип является беззнаковым, значение преобразуется путем многократного добавления или вычитания на единицу большего, чем максимальное значение, которое может быть представлено в новом типе, до тех пор, пока значение не окажется в диапазоне нового типа.
В противном случае новый тип подписывается и значение не может быть в нем представлено; либо результат определяется реализацией, либо возникает сигнал, определяемый реализацией.
Некоторые компиляторы имеют параметр командной строки для изменения поведения подписанного арифметического переполнения с неопределенного поведения на определяемое реализацией:
gcc
а также
clang
служба поддержки
-fwrapv
для принудительного выполнения целочисленных вычислений по модулю 2 32 или 2 64 в зависимости от типа со знаком. Это предотвращает некоторые полезные оптимизации, но также предотвращает некоторые противоречащие интуиции оптимизации, которые могут нарушить невинно выглядящий код. См. Несколько примеров в этом вопросе: Что делает -fwrapv?
Вкратце, применяются следующие правила:
- В 6.7.9 11 говорится, что инициализация ведет себя аналогично простому присваиванию: «… Начальным значением объекта является значение выражения (после преобразования); применяются те же ограничения типа и преобразования, что и для простого назначения,… »
- 6.5.16.1 2 говорит, что простое присваивание выполняет преобразование: «При простом присваивании (
) значение правого операнда преобразуется в тип выражения присваивания и заменяет значение, хранящееся в объекте, обозначенном левым операндом ». - 6.3.1.3 3, который охватывает преобразование в целочисленный тип со знаком, когда значение операнда не может быть представлено в типе, говорит: «либо результат определяется реализацией, либо возникает сигнал, определенный реализацией».
Итак, поведение определено.
В 2018 6.55 есть общее правило об исключительных условиях, возникающих при вычислении выражений:
Если во время оценки выражения возникает исключительное условие (то есть, если результат не определен математически или не входит в диапазон представимых значений для его типа), поведение не определено.
Однако это правило никогда не применяется в приведенной выше цепочке. Выполняя оценки, включая подразумеваемое присвоение инициализации, мы никогда не получаем результат за пределами диапазона его типа. Вход для преобразования находится вне диапазона типа назначения,, но результат конверсии находится в диапазоне, так что нет вне диапазона результата , чтобы вызвать исключительное состояние.
(Возможным исключением из этого является то, что реализация C могла бы, я полагаю, определить результат преобразования, выходящий за пределы диапазона
Это в незаполненном целочисленном переполнении:
int a = UINT_MAX;
Это преобразование беззнакового целочисленного типа со знаком и определяется реализацией. Это описано в разделе 6.3.1.3 стандарта C, касающемся преобразования целочисленных типов со знаком и без знака:
1 Когда значение с целочисленным типом преобразуется в другой целочисленный тип, отличный от
_Bool
, если значение может быть представлено новым типом, оно не изменяется.2 В противном случае, если новый тип является беззнаковым, значение преобразуется путем многократного добавления или вычитания на единицу большего, чем максимальное значение, которое может быть представлено в новом типе, до тех пор, пока значение не окажется в диапазоне нового типа.
3 В противном случае новый тип подписывается и значение не может быть представлено в нем; либо результат определяется реализацией, либо возникает сигнал, определяемый реализацией.
Примером подписанного целочисленного переполнения может быть:
int x = INT_MAX;
x = x + 1;
И это не определено. Фактически, раздел 3.4.3 стандарта C, который определяет неопределенные состояния поведения в параграфе 4:
Пример неопределенного поведения - это поведение при целочисленном переполнении.
И целочисленное переполнение применяется только к типам со знаком согласно 6.2.5p9:
Диапазон неотрицательных значений целочисленного типа со знаком является поддиапазоном соответствующего целочисленного типа без знака, и представление одного и того же значения в каждом типе одинаково. Вычисление с участием беззнаковых операндов никогда не может переполниться, потому что результат, который не может быть представлен результирующим целочисленным типом без знака, уменьшается по модулю числа, которое на единицу больше наибольшего значения, которое может быть представлено результирующим типом.
В ранее существовавшем «языке» (семействе диалектов) стандарт C был написан для описания, реализации обычно либо обрабатывали подписанное целочисленное переполнение, делая то, что делала базовая платформа, усекая значения до длины базового типа (что и является тем, что большинство платформ это сделали) даже на платформах, которые в противном случае делали бы что-то еще или запускали какую-либо форму сигнала или диагностики. В книге K&R «Язык программирования C» поведение описывается как «машинно-зависимое».
Хотя авторы Стандарта заявили, что в опубликованном документе Rationale выявлены некоторые случаи, когда они ожидали, что реализации для обычных платформ будут вести себя обычным образом, они не хотели говорить, что определенные действия будут определять поведение на одних платформах, но не на других. . Кроме того, определение поведения как «определяемого реализацией» создало бы проблему. Рассмотрим что-то вроде:
int f1(void);
int f2(int a, int b, int c);
int test(int x, int y)
{
int test = x*y;
if (f1())
f2(test, x, y);
}
Если бы поведение целочисленного переполнения было «определено реализацией», то любая реализация, в которой она могла бы вызывать сигнал или иметь другие наблюдаемые побочные эффекты, должна была бы выполнить умножение перед вызовом f1(), даже если результат умножения будет проигнорирован если f1() не возвращает ненулевое значение. Классификация его как «Неопределенное поведение» позволяет избежать таких проблем.
К сожалению, gcc интерпретирует классификацию как «Неопределенное поведение» как приглашение обработать целочисленное переполнение способами, не связанными обычными законами причинности. Учитывая такую функцию, как:
unsigned mul_mod_32768(unsigned short x, unsigned short y)
{
return (x*y) & 0x7FFFu;
}
попытка назвать это с