8

Если говорить просто и коротко, то меня интересует: количество и примеры undefined behaviour для каждого из этих типов.

Qwertiy
  • 123,725
MaximPro
  • 3,913
  • Если именно с типами - то теоретически только переполнение в signed. – Harry Mar 02 '17 at 04:27
  • @Harry а подробнее? – MaximPro Mar 02 '17 at 04:28
  • Да в общем-то и все :) Просто поскольку представления знаковых чисел бывают разные, стандарт не ограничивает, как поступать при переполнении - ну, там, при добавлении чего-то к максимальному представимому числу, или, скажем, при сдвиге. В то время как для беззнаковых чисел все это определено. Вообще-то, это даже не столько UB, сколько - зависит от реализации. Для меня как для практика, честно говоря, и то, и другое, и третье :) - все просто темные углы, где стоит знак "сюда не лезь". Поэтому и пишу комментарий, а не ответ, что могу в терминологии путаться. – Harry Mar 02 '17 at 05:06
  • Для знаковых - решают разработчики компилятора, беззнаковые - просто битовый набор. Вот, в тандарте пишут: Unsigned integers shall obey the laws of arithmetic modulo 2^n where n is the number of bits in the value representation of that particular size of integer – Harry Mar 02 '17 at 05:16
  • @Harry вот вы говорите что для беззнаковых определено, а для знаковых нет. Собственно как[ое/ие] представлени[е/я] использу[ется/ются] для беззнаковых, а как[ое/ие] для знаковых – MaximPro Mar 02 '17 at 05:17
  • @Harry с беззнаковыми все понятно. Кстати я все таки залазил в вики и нашел представления знаковых чисел: прямой код, обратный код и дополнительный код. Только вот мне не ясно неужели из-за этих трех представлений весь сыр-бор с undefined behaviour? Если немного углубиться то можно понять что прямой код не используется уже также как и обратный из-за сложности реализации арифметического блока, вроде (надеюсь не наврал). Так смысл тогда в undefined behaviour если остается дополнительный код - единственная реализация знаковых чисел. – MaximPro Mar 02 '17 at 05:26
  • @Harry Кстати про реализацию беззнаковых я так и не понял...но мне кажется что это прямой код, хотя там было сказано если в прямом коде реализованы знаковые числа то алгоритм вычисления арифметических действий усложняется ввиду того что есть отрицательный ноль. ...я в крайнем замешательстве – MaximPro Mar 02 '17 at 05:27
  • прямой это когда 5 = 00000101, -5 = 10000101, очевидно, что тут возникает нуль со знаком 10000000 – vp_arth Mar 02 '17 at 07:12
  • Вам нужен список их всех? Он, боюсь, размером с пол-стандарта. – VladD Mar 02 '17 at 07:57
  • @VladD неужели undefined behaviour ситуаций так много? – MaximPro Mar 02 '17 at 08:49
  • @MaximPro: О да! Из тех, которые не упоминались в ответе: использование неинициализированной переменной = UB, разыменование указателя на локальную переменную, возвращённого из функции = UB, снятие const с const-объекта и его использование = UB, сдвиг на слишком большое количество битов = UB, два вызова delete подряд = UB, невозврат значения из функции = UB (а не ошибка компиляции), присвоение одного поля в union и чтение другого = UB, вычитание указателей внутрь разных массивов = UB, деление MININT на -1 = UB, это только то, что вспомнилось навскидку. – VladD Mar 02 '17 at 20:08
  • @VladD снятие const с const-объекта и его использование = UB? Подробнее, не понял это как? – MaximPro Mar 03 '17 at 04:57
  • @MaximPro: Например, если объект декларирован как const T, вы получаете на него указатель типа const T *, снимаете с него константность через const_cast, и через полученный указатель модифицируете, это UB. А вот если объект не был декларирован как const T, тогда UB нет. Но в точке, где вы делаете const_cast, часто вы не знаете, был ли объект на деле константой. – VladD Mar 03 '17 at 08:12
  • @VladD а можно небольшой пример кода, а то не совсем понятно – MaximPro Mar 03 '17 at 16:38
  • @MaximPro: Ну например: struct A { int b; }; const A a; const A* pa = &a; const_cast<A*>(pa)->b = 5; — здесь UB. – VladD Mar 03 '17 at 23:21
  • @VladD понятно! – MaximPro Mar 04 '17 at 00:46

2 Answers2

10
  • Переполнение при выполнении арифметических операций над типом signed int приводит к неопределенному поведению.

  • Объектные представления как signed int, так и unsigned int могут иметь в своем составе padding биты, т.е. биты, не участвующие в формировании значения, а либо выполняющие вспомогательные функции, либо вообще не использующиеся. Комбинация значений padding битов может быть некорректной, т.е. формировать так называемые trap representations. Попытка доступа к trap representation приводит к неопределенному поведению. (Например, объектное представление целочисленного типа может содержать биты четности.)

    Язык, однако, гарантирует, что установка всех битов объектного представления целочисленного типа в 0 (в т.ч. padding битов) не может создать trap representation, а всегда приводит к формированию корректно представленного целочисленного значения 0. С практической точки зрения это означает, что memset(..., 0, ...) и calloc гарантированно формируют правильные нулевые значения для любых целочисленных типов.

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

  • Реализации, основанные на прямом или обратном коде для signed int, имеют право запретить использование отрицательного нуля, т.е. расценивать представление отрицательного нуля как trap representation. В таком случае доступ к представлению отрицательного нуля приводит к неопределенному поведению.

  • Реализации, основанные на дополнительном коде для signed int, имеют право запретить использование представления с 1 в знаковом бите и с 0 во всех значащих битах, т.е. расценивать это представление как trap representation. В таком случае доступ к такому представлению приводит к неопределенному поведению. (Другими словами, в 16-битном signed int значение -32768 может быть "запрещено".)

  • Почему при выполнении арифметических операциях над типом signed int приводит к неопределенному поведению? Чем это выражается? И почему при unsigned int нет неопределенного состояния? – MaximPro Mar 02 '17 at 10:45
  • И не понятен последний абзац. Что значит запретить использование представления. Как можно запретить использование дополнительного кода? И что тогда получится? Я ничего не понимаю. Объясните – MaximPro Mar 02 '17 at 15:03
  • @MaximPro: "приводит к неопределенному поведению" потому что так сказано в стандарте. А в стандарте так сказано потому, что 1) в рамках разнообразия представлений никакого способа определить результат нет, 2) существуют/существовали платформы, которые отлавливают знаковые переполнения и выкидывают ошибку, 3) еще масса разных причин, которые все и не упомнишь. – AnT stands with Russia Mar 02 '17 at 15:40
  • @MaximPro: Где вы увидели "запретить использование дополнительного кода" мне не ясно. Речь идет о запрете конкретных комбинаций битов. Попытаетесь прочитать из памяти такую запретную комбинацию, как значение типа int - получите неопределенное поведение. – AnT stands with Russia Mar 02 '17 at 15:42
  • Я несколько неправильно выразил мысль.У вас написано trap representation. Что расцеивается как этот "trap"? Цитата: Реализации, основанные на дополнительном коде для signed int, имеют право запретить использование представления с 1 в знаковом бите и с 0 во всех значащих битах. Погодите, но это же дополнительный код как можно расценивать 1000 0000 0000 0000 как trap? Я бы понял если бы это был прямой код или обратный, но тут же...как??? – MaximPro Mar 02 '17 at 17:12
  • @MaximPro: "Trap" - это исторический термин, обозанчающий "исключение" и/или "прерывание". В чем вы видите проблему с 10...0 мне в упор не ясно. Любая реализация с дополнительным кодом может сказать: у нас тип signed int имеет диапазон не [-32768, +32767], а диапазон [-32767, +32767] (симметричный). А бывшее представление для -32768 (т.е. 10...0) мы резервируем в качестуве trap-представления. При попытке работы с эти предствлением будет вызникать исключение ("trap"). Вот и все. Что тут непонятного? – AnT stands with Russia Mar 02 '17 at 17:47
  • А зачем этот trap? Нафига он тут нужен? С таким успехом можно и 1111 1111 1111 1111 в трап записать или еще что-нибудь? Я просто не вижу логического смысла в этом? Зачем запрещать код 1000 0000 0000 0000? Когда сказано что в обратном представлении это число -32768. – MaximPro Mar 02 '17 at 18:02
  • @MaximPro: Во-первых, зачем он нужен - догадайтесь сами. Существует миллион хороших причин иметь "зарезервированое плохое значение". Например, для целей пометки неинициализированных или неактульных значений (отдаленно аналогично тот же null-указателю). – AnT stands with Russia Mar 02 '17 at 18:13
  • Во-вторых, где это сказано, что 100..0 в доп.коде - это -32768? Каким образом вы это вычислили? На самом деле, нет однозначного ответа на вопрос, обозначает ли 100..0 -32768 или все таки +32768. Можно привести агрументы в пользу обеих интерпретаций. Одно правило смены знака говорит, что надо взять дополнение до 2^N. Другое правило смены знака говорит, что надо инвертировать значащие биты и прибавить к результату 1. Оба правила переполняются при работе с числом -32768 в 16 битах. – AnT stands with Russia Mar 02 '17 at 18:19
  • Ну там не совсем -32768, но вот допустим тут https://ru.wikipedia.org/wiki/%D0%94%D0%BE%D0%BF%D0%BE%D0%BB%D0%BD%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D0%BA%D0%BE%D0%B4_(%D0%BF%D1%80%D0%B5%D0%B4%D1%81%D1%82%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5_%D1%87%D0%B8%D1%81%D0%BB%D0%B0) и в доп.коде так и написано 1000 0000 (-128). Что значит:Оба правила переполняются при работе с числом -32768 в 16 битах? – MaximPro Mar 02 '17 at 18:20
  • Т.е. ясно, что это представление - "кривоватое". Уже сам факт того, что оно не имеет симметричного положительного "брата" и вызывает переполнение при умножении или делении на -1 говорит о его "кривости". – AnT stands with Russia Mar 02 '17 at 18:20
  • Ну, а что насчет unsigned типа? В каком он виде представляется, я как посмотрю для него нет неопределенного поведения. – MaximPro Mar 02 '17 at 18:33
  • @MaximPro: Беззнаковые типы представляются в чистой двоичной записи (pure binary notation). Никакие "дополнительные коды", "обратные коды" и т.п. никакого отношения к беззнаковым типам не имеют - они там нинафиг не нужны. – AnT stands with Russia Mar 02 '17 at 18:47
  • И даже прямой код не используется? – MaximPro Mar 02 '17 at 18:50
  • @MaximPro: Ым.. "Прямой код", "обратный код", "дополнительный код" - это все, по определению, способы представления знаковых значений в двоичном коде. Все эти представления фундаментально основаны на использовании знакового бита. К беззнаковым значениям они вообще никаким боком не относятся и не могут относиться. Хотя бы потому, что там никакого знакового бита нет и в помине. – AnT stands with Russia Mar 02 '17 at 18:56
1

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

Битовые сдвиги (далее, результирующий тип — это тип левого операнда, подвергнутый целочисленному продвижению (integral promotion)). ([expr.shift] 8.5.7)

  • Если правый операнд битового сдвига отрицательный, больше или равен длине в битах продвинутого (promoted) левого операнда — UB.

  • Если отрицательная знаковая величина сдвигается влево — UB.

  • Если неотрицательная знаковая величина E1 сдвигается влево на E2 бит, и значение E1 * (2^E2) не может быть представлено в беззнаковом целочисленном типе, соответствующем результирующему типу, то UB.

  • Если неотрицательная знаковая величина E1 сдвигается влево на E2 бит, и значение E1 * (2^E2) может быть представлено в беззнаковом целочисленном типе, соответствующем результирующему типу, но не может быть представлено в результирующем типе, то значение E1 * (2^E2) преобразуется к результирующему типу. Результат такого преобразования определяется реализацией.

  • Если отрицательная знаковая величина сдвигается вправо, то результирующее значение определяется реализацией.

  • Если в результате некоторой битовой операции (не только сдвиги) генерируется trap representation, то поведение такой битовой операции не определено. (C 6.2.6.2 / 4)

Арифметика указателей. ([expr.add] 8.5.6)

  • Если выражение P указывает на элемент x[i] массива x из n элементов, то выражения P + J и J + P (где J имеет целочисленное значение j) указывает на элемент x[i + j] только если 0 <= i + j <= n, в противном случае — UB.

  • Выражение P - J указывает на элемент x[i - j] только если 0 <= i - j <= n, в противном случае — UB.

  • Если выражение P указывает на элемент x[i] массива x, а выражение Q указывает на элемент x[j] этого же массива, то результат выражения P - Q равен знаковому целочисленному значению i - j типа std::ptrdiff_t. Если P и Q указывают на элементы разных массивов — UB. Если числовое значение i - j не представимо типом std::ptrdiff_t — UB. Стандарт языка определяет диапазон значений типа std::ptrdiff_t ссылаясь на стандарт языка C, согласно которому этот тип должен вместить все значения из диапазона [-65535, 65535]. Стандарт не требует, чтобы std::ptrdiff_t мог вместить все значения типа size_t. ([cstdint.syn] 21.4.1, C 7.20.3 / 2)

Указатели.

  • Когда время жизни некоторой области памяти подходит к концу, то все указатели, указывающие на любую часть этой области памяти становятся недействительными (invalid pointer value). Разыменование или высвобождение памяти по такому указателю — UB. ([basic.stc] 6.6.4 / 4)

  • Последствия любого другого использования invalid pointer value — определяются реализацией. В частности, в стандарте явно оговорено, что аварийное завершение работы программы при выполнении следующего кода — нормальное поведение ([basic.stc] 6.6.4 35)):

    int *p1, *p2;
    p1 = new int;
    delete p1;
    p2 = p1; //system-generated runtime fault;
    
  • Если указатель на объектный тип T1 приводится к указателю на объектный тип T2, и требования по выравниванию (alignment requirements) для типа T2 не выполняются, то результирующее указательное значение не специфицируется (unspecified). Полагаю, в частности, может получиться invalid pointer value со всеми вытекающими. ([expr.reinterpret.cast] 8.5.1.10 / 7, [expr.static.cast] 8.5.1.9 / 13)

Ну и если речь зашла о неопределённом поведении, то как не упомянуть следующие пункты:


Естественно, приведённые примеры — это далеко не полный список неопределённых, определяемых реализацией и неспецифицированных поведений :)

wololo
  • 6,221