Четко ли определено переполнение intN_t?

В C99 есть некоторые (необязательные) типы, такие как int8_t, int16_t и им подобные, которые гарантированно имеют точно заданную ширину и не содержат битов заполнения, а также представляют числа в дополнении до двух (7.18.1.1). В 6.2.6.2 переполнение целого числа со знаком упоминается как сноски 44) и 45), а именно то, что оно может привести к перехвату значений в битах заполнения.

Поскольку intN_t не имеет битов заполнения, и они гарантированно являются дополнением до двух, означает ли это, что их переполнение не приводит к неопределенному поведению? Каков будет результат, например. переполненное умножение? Что насчет дополнения? Уменьшается ли результат по модулю 2^N, как для беззнаковых типов?


person Ruslan    schedule 14.06.2015    source источник
comment
IANALL, но даже несмотря на то, что они представлены с дополнением до 2, они по-прежнему не подчиняются законам арифметики по модулю 2^n, поэтому результат переполнения не определяется математически. И стандарт говорит, что если во время оценки выражения результат не определен математически или не находится в диапазоне представляемых значений для его типа, поведение не определено.   -  person tux3    schedule 14.06.2015
comment
Даже для uintN_t переполнение не (обычно) четко определено. И intN_t, и uintN_t по-прежнему подлежат целочисленному преобразованию, и умножение двух значений uint16_t в системах, где uint16_t преобразуется в int, может привести к результату, который не может быть представлен в int.   -  person    schedule 14.06.2015
comment
Примечание. Такие типы, как int8_t, int16_t, не являются полностью необязательными в C99: эти типы являются необязательными. Однако, если реализация предоставляет целочисленные типы шириной 8, 16, 32 или 64 бита, без битов заполнения и (для типов со знаком) с представлением в дополнительном коде до двух, она должна определить соответствующие имена typedef. §7.20.1.1 3   -  person chux - Reinstate Monica    schedule 14.06.2015
comment
@hvd: Интересно, возникнут ли какие-либо практические трудности с требованием, чтобы в случаях, когда результат аддитивного, мультипликативного, побитового оператора или оператора сдвига влево приводился или приводился к беззнаковому типу, меньшему, чем int, компилятор должен либо генерировать код, который будет вести себя так, как если бы оператор принудил свои операнды к этому типу и выполнил операцию, используя этот тип, или иначе определить __STDC_NON_MODULAR_UNSIGNED_SHORT. Обратите внимание, что почти все компиляторы, выпущенные до 2010 года (и многие выпущенные позже), уже соответствуют этому правилу, и большинство из них можно заставить соответствовать...   -  person supercat    schedule 16.06.2015
comment
...используя параметр командной строки. Правило не требует каких-либо изменений в любом поведении, определенном Стандартом, и маловероятно, что оно будет противоречить какому-либо определенному поведению каких-либо расширений компилятора. Единственным эффектом правила будет изменение некоторых видов неопределенного поведения, что приведет к полезной оптимизации поведения, независимого от платформы.   -  person supercat    schedule 16.06.2015
comment
@supercat Пример, который я привел для (unsigned short) ((unsigned short) 65535 * (unsigned short) 65535), - это случай, когда текущие часто используемые реализации не соответствуют предложенному вами правилу, по крайней мере, в некоторых из их соответствующих режимов, и из-за этого макрос функции не был бы очень полезен. Кроме того, я думаю, что макрос функции должен быть наоборот: если макрос не определен, вы не знаете, потому ли это, что он поддерживает операции, или потому, что он придерживается более старого стандарта. Сделать это __STDC_MODULAR_UNSIGNED_SHORT: если это определено, вы знаете, что это сработает.   -  person    schedule 16.06.2015
comment
@hvd: В идеале тесты должны существовать в обоих направлениях, но с точки зрения стандарта я бы посчитал отрицательное направление более важным. Добавление #if (__STDC_MODULAR_UNSIGNED_SHORT>0) #error ... к коду, который в настоящее время используется с устаревшим компилятором, где неподписанные шорты всегда работали должным образом, не повлияет на его поведение в этом компиляторе, но гарантирует, что если он будет перемещен в более новый компилятор, он либо будет работать, как ожидалось, либо выйдет из строя. компиляция. Только компиляторы, которые достаточно новы, чтобы проявить творческий подход, но недостаточно новы, чтобы об этом сказать, могут создавать проблемы.   -  person supercat    schedule 16.06.2015
comment
@hvd: я бы также сказал, что гораздо лучше иметь язык, в котором x*=x; безусловно безопасен, и программист, который готов принять неопределенное поведение, когда x выходит за пределы, может написать x=(int)(x*x), чем тот, где программисты, желающие нормального поведения, должны писать x*=1u*x;, и где неспособность написать это приведет к тому, что программа будет часто работать в любом случае (даже с запредельными значениями x), но может в какой-то более поздний момент перестать подчиняться законам времени и причинно-следственной связи.   -  person supercat    schedule 16.06.2015
comment
@hvd: я бы также сказал, что с компилятором, который обещал соблюдать очень свободное, но не полностью неограниченное поведение при переполнении, было бы возможно больше оптимизаций, чем было бы возможно при компиляции программы, которая воспользовалась этим обещанием. программа, которая была написана, чтобы абсолютно точно избежать переполнения ни при каких обстоятельствах. Если аудиовизуальному декодеру требуется заполнить выходной буфер допустимым выводом при правильном вводе, и он может записывать все, что ему нравится, в выходной буфер при недопустимом вводе, но не должен записывать вне этого...   -  person supercat    schedule 16.06.2015
comment
... тогда наличие слегка ограниченной семантики переполнения сведет к минимуму объем захвата переполнения, который декодер должен был бы сделать, чтобы удовлетворить его запрет на запись вне ограничения буфера. Точный захват переполнения стоит дорого, поскольку исключает многие формы переупорядочения циклов; более свободная семантика может быть намного дешевле. Если приложению все равно, будут ли данные обрабатываться после того, как произойдет переполнение, при условии, что за пределы буфера ничего не записывается, такой точный захват представляет собой напрасную трату усилий как на человеческом, так и на машинном уровне.   -  person supercat    schedule 16.06.2015
comment
@supercat У вас есть конкретный пример оптимизации, которая была бы возможна в хорошо написанной программе в вашей гипотетической будущей версии C, но невозможна в хорошо написанной программе в текущей версии C? Я не верю, что такая вещь существует (даже если хорошо написанная программа в текущей версии C может быть более уродливой), но я буду счастлив, если мне докажут, что я не прав.   -  person    schedule 17.06.2015
comment
@hvd: typedef struct { int32_t left, top, bottom, right; } RECT; int32_t bb_right(RECT *rects, int32_t count) { int32_t max_x = INT_MIN; for (int32_t i=0; i<count; i++) { int32_t r=rects[i].left + rects[i].width; if (r > max_x) max_x = r; } return max_x;}. Функция должна возвращать крайнюю правую координату заданных прямоугольников, если ширина каждого прямоугольника неотрицательна и не превышает INT32_MAX-left. Если функции разрешено возвращать любое произвольное значение, но нет других побочных эффектов, при указании указателя на доступный массив, содержащий недопустимые прямоугольники...   -  person supercat    schedule 17.06.2015
comment
... как можно написать код на стандартном C таким образом, чтобы можно было ожидать, что он будет таким же быстрым, как и выше, в случаях, когда нет переполнения? Вычисление uint32_t r = 0x80000000u+rects[i].left+rects[i].width; и определение вычисленного таким образом максимального r будет работать, но я не думаю, что программист имеет право ожидать, что это будет так же быстро, как оригинал (возможно, некоторые оптимизаторы могли бы понять, что они могут избежать дополнительного добавления 0x80000000, если они использовать операцию сравнения со знаком, но это кажется чрезмерно оптимистичным).   -  person supercat    schedule 17.06.2015
comment
Если кто-то хочет добавить расширение языка помимо того, что традиционно делают многие компиляторы, рассмотрите требование: int32_t sum(int32_t *dat, int32_t count);. Возвращает итог, если промежуточное значение не переполняется. Возвратите INT32_MIN, если результат не может быть представлен в int32_t. Произвольно вернуть либо правильную сумму, либо INT32_MIN, если промежуточные значения не помещаются в int32_t, но сумма может. Есть ли какой-либо способ написать код C, который дал бы что-то близкое к оптимальному коду, отвечающему этим требованиям как на 32-битных, так и на 64-битных платформах, имея в виду, что...   -  person supercat    schedule 17.06.2015
comment
... на 64-битной платформе, вероятно, было бы быстрее всего, если бы код не обращал внимания на то, подходит ли что-либо, кроме конечного результата, в int32_t, но, например, ARM7-TDMI цикл может быть развернут для использования инструкции загрузки-множества, последовательности инструкций сложения, если не переполнения, инструкции вычитания 1, если не переполнения на счетчике (предварительно инициализированного, поэтому последний цикл перенесет его от 0x80000000 до 0x7FFFFFFFF) и ветвь, если не переполнение. Гораздо быстрее, чем оптимальный код для вычисления 64-битного итога.   -  person supercat    schedule 17.06.2015
comment
Причина, по которой программисты принимают языки, в которых переполнение может остаться незамеченным, заключается в том, что точный захват обходится дорого. Если язык позволяет программистам указать, насколько сильно они должны защищать вещи, компилятор может добавить проверку переполнения с гораздо меньшими затратами (для приведенного выше примера только небольшая стоимость постоянного времени (ноль затрат на цикл!), чтобы добавить проверка переполнения для всех значений в цикле как для x64, так и для ARM7-TDMI; немного больше затрат на x86, но меньше, чем было бы необходимо для программы, написанной на сегодняшнем C).   -  person supercat    schedule 17.06.2015
comment
@supercat Вы предлагали макрос функции, который эффективно сообщает, поддерживается ли модульная арифметика для типов uintN_t. Мои комментарии были ответом на это предложение. Если ваш конкретный пример того, почему этот макрос функции хорош, работает только для intN_t, тогда может сработать другое предложение, но не то, которое вы на самом деле сделали.   -  person    schedule 17.06.2015
comment
@hvd: я думал, что ваш вопрос о конкретных примерах был направлен на мой более общий комментарий о том, что свободная, но ограниченная семантика переполнения будет полезна. Что касается моего конкретного предложения здесь для коротких значений без знака, я намерен отказаться от некоторых оптимизаций, которые очень редко будут полезны, в обмен на возможность иметь существующий код, который правильно работает для любого размера int правильно работает со всеми размерами int. Сказать, что foo *= foo; будет иметь тот же эффект, что и foo *= 1u*foo; во всех случаях, где оно определено, но может свести на нет законы времени и...   -  person supercat    schedule 18.06.2015
comment
... причинно-следственная связь на некоторых машинах не очень полезна. Если причина оставить его открытым состоит в том, чтобы облегчить оптимизацию, то органы по стандартизации могли бы сделать и другие вещи с целочисленным переполнением, что облегчило бы гораздо больше полезных оптимизаций, не делая код излишне зависимым от размера int; моя предпочтительная нормативная семантика неявно заставляла бы работать модульную арифметику с короткими беззнаковыми значениями, даже если компиляторы не имели для этого специальной обработки (и были бы полезны в других местах), но на самом деле ничего не получается, взламывая существующий код.   -  person supercat    schedule 18.06.2015


Ответы (2)


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

Так что нет, это не означает, что переполнение определено. Для операций, включающих intN_t/uintN_t операндов, возможно переполнение, и это переполнение может привести к неопределенному поведению.

Что-то вроде int8_t i = 127; ++i; не имеет UB. int8_t подлежит интегральным акциям, поэтому добавление осуществляется так, как если бы вы написали i = (int8_t) ((int) i + 1);. Само добавление не переполняется, а преобразование обратно в int8_t дает результат, определяемый реализацией.

Что-то вроде uint16_t u = 65535; u *= u; имеет UB в текущих типичных реализациях, где int имеет 32 бита знака/значения. uint16_t тоже подлежит интегральным преобразованиям, поэтому умножение выполняется так, как если бы вы написали u = (uint16_t) ((int) u * (int) u);. Само умножение переполняется, и поведение не определено.

Что-то вроде int64_t i = 9223372036854775807; ++i; имеет UB практически на всех реализациях. Само дополнение переливается.

person Community    schedule 14.06.2015
comment
Разве u не будет повышен до unsigned int, а не int? - person Oliver Charlesworth; 14.06.2015
comment
@OliverCharlesworth Только в том случае, если int не может представлять все значения типа. Например, если uint16_t равно unsigned short, а unsigned short и unsigned int имеют одинаковый размер и диапазон. Но если int достаточно велико, то оно будет использоваться. Давным-давно все было по-другому, но, насколько я знаю, это было до первого стандарта C, первый стандарт C требовал, чтобы продвижение работало так, как я здесь описываю, и с тех пор он никогда не менялся. - person ; 14.06.2015
comment
Я полагаю, что с uint16_t u = 65535;, u *= u --> u *= u*1U; устранит UB. Я думаю, что трюк с *1U также помогает с более широкими uintN_t. - person chux - Reinstate Monica; 14.06.2015
comment
@chux Да, это работает. Я также рекомендовал умножение на 1U здесь, на SO: >32-битное беззнаковое умножение на 64-битное, вызывающее неопределенное поведение? :) - person ; 14.06.2015
comment
Беззнаковые арифметические операции переполняются. Неопределенное поведение предназначено только для подписанных. - person bzim; 17.02.2018
comment
@BrunoCorrêaZimmermann Стандарт описывает это не так. См. C99 3.4.3p3 (ненормативный), дающий целочисленное переполнение, а не целочисленное переполнение со знаком, в качестве примера UB. Но см. C99 6.2.5p9 (нормативный), в котором говорится, что переполнение целого числа без знака не существует, потому что это было бы переполнением только в том случае, если результат был бы вне диапазона после сокращения по модулю 2ⁿ. - person ; 17.02.2018
comment
О, мой плохой. Да, технически нет переполнения из-за модуля. - person bzim; 18.02.2018

Нет, это не определено, потому что... оно вообще не определено. В стандарте просто нет текста, который придавал бы семантику переполнению целого числа со знаком.

Не преувеличивайте значение термина «неопределенное поведение» как нечто таинственное, а воспринимайте его в прямом смысле. Определения поведения нет, поэтому в стандарте ничего не указано, что должно произойти, и ни один переносимый код не должен полагаться на такую ​​функцию.

person Jens Gustedt    schedule 14.06.2015
comment
Стандарт указывает, что типы intXX_t должны храниться с использованием представления с дополнением до двух и без битов заполнения, а также указывает, что преобразование типа int в меньший тип int, который не может хранить свое значение, должно вестись в соответствии с реализацией. Я предполагаю, что реализация могла бы использовать дополнение до двух, но определить для преобразования что-то иное, чем сокращение с дополнением до двух, но это было бы очень необычно. Однако результат не является UB в любом случае, когда четко определенный int преобразуется в меньший размер. - person supercat; 16.06.2015
comment
@supercat, вопрос о переполнении, а не о преобразовании. - person Jens Gustedt; 17.06.2015
comment
Учитывая int16_t x=182; в системе, где INT_MAX равно 65535 или больше, тот факт, что x*=x; сделает x равным -32412, не будет переполнением, как этот термин определен в стандарте, но если бы x ожидалось, что он будет вести себя как число, то, скорее всего, естественно сказать, что он переполнялся. - person supercat; 17.06.2015