Я обнаружил чрезвычайно неприятную ошибку, скрывающуюся за этой маленькой жемчужиной. Я знаю, что согласно спецификации C ++ подписанные переполнения являются неопределенным поведением, но только когда переполнение происходит, когда значение расширяется до разрядности sizeof(int)
. Насколько я понимаю, увеличение a char
никогда не должно быть неопределенным, пока sizeof(char) < sizeof(int)
. Но это не объясняет, как c
получить невозможную ценность. Как 8-битное целое число может c
содержать значения, превышающие его разрядность?
Код
// Compiled with gcc-4.7.2
#include <cstdio>
#include <stdint.h>
#include <climits>
int main()
{
int8_t c = 0;
printf("SCHAR_MIN: %i\n", SCHAR_MIN);
printf("SCHAR_MAX: %i\n", SCHAR_MAX);
for (int32_t i = 0; i <= 300; i++)
printf("c: %i\n", c--);
printf("c: %i\n", c);
return 0;
}
Вывод
SCHAR_MIN: -128
SCHAR_MAX: 127
c: 0
c: -1
c: -2
c: -3
...
c: -127
c: -128 // <= The next value should still be an 8-bit value.
c: -129 // <= What? That's more than 8 bits!
c: -130 // <= Uh...
c: -131
...
c: -297
c: -298 // <= Getting ridiculous now.
c: -299
c: -300
c: -45 // <= ..........
Зацените на идеоне.
c++
gcc
undefined-behavior
неподписанный
источник
источник
printf()
преобразование?Ответы:
Это ошибка компилятора.
Хотя получение невозможных результатов для неопределенного поведения является допустимым следствием, на самом деле в вашем коде нет неопределенного поведения. Что происходит, так это то, что компилятор считает, что поведение не определено, и соответственно оптимизирует его.
Если
c
определено какint8_t
, иint8_t
повышает доint
, тоc--
предполагается, чтоc - 1
вint
арифметике выполняется вычитание и преобразование результата обратно вint8_t
. Вычитаниеint
не приводит к переполнению, и преобразование целочисленных значений вне диапазона в другой целочисленный тип допустимо. Если тип назначения подписан, результат определяется реализацией, но он должен быть допустимым значением для типа назначения. (И если тип назначения беззнаковый, результат четко определен, но здесь это не применяется.)источник
c
в более широком типе. Предположительно, вот что здесь происходит.В компиляторе могут быть ошибки, отличные от несоответствия стандарту, потому что есть другие требования. Компилятор должен быть совместим с другими версиями самого себя. Также можно ожидать, что он будет в некоторой степени совместим с другими компиляторами, а также будет соответствовать некоторым представлениям о поведении, которых придерживается большая часть его пользовательской базы.
В этом случае это похоже на ошибку соответствия. Выражение
c--
должно обрабатыватьсяc
аналогичноc = c - 1
. Здесь значениеc
справа повышается до типаint
, а затем происходит вычитание. Посколькуc
находится в диапазонеint8_t
, это вычитание не приведет к переполнению, но может дать значение, выходящее за пределы диапазонаint8_t
. Когда это значение присваивается, происходит обратное преобразование к типу,int8_t
чтобы результат снова соответствовалc
. В случае выхода за пределы допустимого диапазона преобразование имеет значение, определяемое реализацией. Но значение вне диапазонаint8_t
не является допустимым значением, определяемым реализацией. Реализация не может «определить», что 8-битный тип внезапно содержит 9 или более бит. Значение, определяемое реализацией, означает, чтоint8_t
создается что-то в диапазоне от , и программа продолжается. Таким образом, стандарт C допускает такие варианты поведения, как арифметика насыщения (обычная для DSP) или циклическая обработка (обычные архитектуры).Компилятор использует более широкий базовый тип машины при манипулировании значениями небольших целочисленных типов, таких как
int8_t
илиchar
. Когда выполняется арифметика, результаты, выходящие за пределы диапазона малых целых чисел, могут быть надежно зафиксированы в этом более широком типе. Чтобы сохранить внешне видимое поведение, когда переменная является 8-битным типом, более широкий результат должен быть усечен до 8-битного диапазона. Для этого требуется явный код, так как ячейки памяти машины (регистры) шире 8 бит и подходят для больших значений. Здесь компилятор не позаботился о нормализации значения и просто передал егоprintf
как есть. Спецификатор преобразования%i
вprintf
не знает, что аргумент изначально получен в результатеint8_t
вычислений; он просто работает сint
аргумент.источник
Я не могу вписать это в комментарий, поэтому отправляю его как ответ.
По какой-то очень странной причине
--
виновником оказывается оператор.Я протестировал код, опубликованный на Ideone, и заменил его
c--
на,c = c - 1
а значения остались в диапазоне [-128 ... 127]:Странный эй? Я мало что знаю о том, что компилятор делает с такими выражениями, как
i++
илиi--
. Вероятно, он продвигает возвращаемое значение вint
и передает его. Это единственный логический вывод, который я могу сделать, потому что вы на самом деле получаете значения, которые не могут поместиться в 8-битные.источник
c = c - 1
значитc = (int8_t) ((int)c - 1
. Преобразование вне диапазонаint
вint8_t
определенное поведение, но результат определяется реализацией. На самом деле, разве неc--
должны выполняться те же преобразования?Я предполагаю, что базовое оборудование все еще использует 32-битный регистр для хранения этого int8_t. Поскольку спецификация не налагает поведения на переполнение, реализация не проверяет переполнение и позволяет также сохранять большие значения.
Если вы помечаете локальную переменную, поскольку
volatile
вы заставляете использовать для нее память и, следовательно, получаете ожидаемые значения в пределах диапазона.источник
printf
не говоря уже о том,sizeof
чтобы не заботиться о значениях формата.Код ассемблера раскрывает проблему:
EBX следует дополнить пост-декрементом FF, или следует использовать только BL с оставшейся частью EBX. Любопытно, что он использует sub вместо dec. -45 просто загадочна. Это побитовая инверсия 300 & 255 = 44. -45 = ~ 44. Где-то есть связь.
При использовании c = c - 1 требуется гораздо больше работы:
Затем он использует только нижнюю часть RAX, поэтому он ограничен значениями от -128 до 127. Параметры компилятора «-g -O2».
Без оптимизации он производит правильный код:
Так это ошибка оптимизатора.
источник
Используйте
%hhd
вместо%i
! Должен решить вашу проблему.То, что вы видите, является результатом оптимизации компилятора в сочетании с вашим указанием printf напечатать 32-битное число, а затем помещением (предположительно 8-битного) числа в стек, который на самом деле имеет размер указателя, потому что именно так работает код операции push в x86.
источник
g++ -O3
. Переход%i
на%hhd
ничего не меняет.Я думаю, это происходит за счет оптимизации кода:
Компилятор использует
int32_t i
переменную как для, такi
и дляc
. Отключите оптимизацию или сделайте прямую трансляциюprintf("c: %i\n", (int8_t)c--);
источник
(int8_t)(c & 0x0000ffff)--
c
сам определяется какint8_t
, но при работе++
или--
вышеint8_t
он сначала неявно преобразуется в,int
и вместо этого в результате операции внутреннее значение c печатается с помощью printf, что и естьint
.См фактического значения из
c
после всего цикла, особенно после последнего декрементаэто правильное значение, которое напоминает поведение
-128 + 1 = 127
c
начинает использоватьint
память размеров, но печатается какint8_t
при печати, как сама, используя только8 bits
.32 bits
Использует все, когда используется какint
[Ошибка компилятора]
источник
Я думаю, это произошло, потому что ваш цикл будет идти до тех пор, пока int i не станет 300, а c станет -300. И последнее значение потому, что
источник