Как определить переполнение при добавлении signed в unsigned

#c #overflow #unsigned #signed #underflow

#c #переполнение #неподписанный #подписано #недостаточный поток

Вопрос:

Я пытаюсь обнаружить переполнение при добавлении смещения со знаком в позицию без знака

 uint32 position;
int32 offset;  // it could be negative
uint32 position = position offset;
  

Как я могу проверить, является ли результат переполнением или переполнением?

Я придумал уродливый способ, но не уверен в его правильности.

  • недостаточный поток: offset < 0 amp;amp; position offset >= position
  • переполнение: offset > 0 amp;amp; position offset <= position

И мне также интересно, есть ли более элегантный способ сделать это.

Обновить:

Какое лучшее решение, если смещение длинное?

 uint32 position;
long offset;  // it could be negative
uint32 position = position offset;
  

Ответ №1:

Ваши тесты верны. Я не вижу более элегантного способа прямо сейчас, возможно, его нет.

Почему условия верны: арифметика on uint32_t — это арифметика по модулю 2 ^ 32. Преобразование из int32_t в uint32_t обычно представляет собой переосмысление битового шаблона (в любом случае, как указал @caf, здесь это уменьшение по модулю 2 ^ 32, так что это определенно работает). Рассматривайте position и offset как целые числа произвольной точности. Переполнение происходит тогда и только тогда, когда
position offset >= 2^32 . Но offset < 2^31 , таким position offset < position 2^31 образом , что меньше, чем position 2^32 , следующее значение, которое уменьшается по position модулю 2 ^ 32, так как uint32_t , тогда position offset < position . С другой стороны, если offset > 0 и position offset < position , очевидно, произошло переполнение. Переполнение происходит тогда и только тогда position offset < 0 , когда в виде математических целых чисел. Поскольку offset >= -2^31 аналогичные рассуждения показывают, что переполнение произошло тогда и только тогда, когда offset < 0 amp;amp; position offset > position .

Комментарии:

1. Преобразование отрицательных чисел в unsigned не определяется реализацией — оно уменьшается по модулю на единицу больше максимального значения, представимого в типе unsigned . Это гарантируется стандартом.

2. О, хорошо, я тогда неправильно запомнил. Спасибо за предупреждение.

3. @DanielFischer, смещение будет преобразовано в uint32_t при добавлении в позицию. Так что на самом деле смещение <=2 ^ 32-1. Спасибо за ваше объяснение.

4. В этой части вы должны были рассматривать их как математические целые числа. Было недостаточно ясно 🙁

5. @DanielFischer Вы правы. Он находится в ветви смещения> 0.

Ответ №2:

Следующая функция проверяет переполнение / переполнение при добавлении int32_t в uint32_t. Он также содержит некоторые тестовые примеры в качестве доказательства правильности.

 #include <stdint.h>
#include <assert.h>

int is_overflow (uint32_t position, int32_t offset)
{
    if (offset > 0 amp;amp; offset > UINT32_MAX - position) {
        // really we checked (offset   position > UINT32_MAX)
        // overflow
        return 1;
    }
    else if (offset < 0 amp;amp; (uint32_t)offset <= UINT32_MAX - position) {
        // really we checked  (position   (uint32_t)offset <= UINT32_MAX)
        // the (uint32_t)offset maps negative offset to [2^31, UINT32_MAX]
        // underflow
        return -1;
    }

    // no over/underflow
    return 0;
}

uint32_t abs_of_negative_int32 (int32_t offset)
{
    assert(offset < 0);

    return ((UINT32_MAX - (uint32_t)offset)   1);
}

int main (int argc, char *argv[])
{
    int r;

    r = is_overflow(0, 0);
    assert(r == 0);

    r = is_overflow(0, 1);
    assert(r == 0);

    r = is_overflow(0, INT32_MAX - 1);
    assert(r == 0);

    r = is_overflow(0, INT32_MAX);
    assert(r == 0);

    r = is_overflow(0, -1);
    assert(r == -1);

    r = is_overflow(0, INT32_MIN   1);
    assert(r == -1);

    r = is_overflow(0, INT32_MIN);
    assert(r == -1);

    r = is_overflow(UINT32_MAX, 0);
    assert(r == 0);

    r = is_overflow(UINT32_MAX, 1);
    assert(r == 1);

    r = is_overflow(UINT32_MAX - 1, 1);
    assert(r == 0);

    r = is_overflow(UINT32_MAX - 1, 2);
    assert(r == 1);

    r = is_overflow(UINT32_MAX - 1, INT32_MAX);
    assert(r == 1);

    r = is_overflow(UINT32_MAX - INT32_MAX, INT32_MAX);
    assert(r == 0);

    r = is_overflow(UINT32_MAX - INT32_MAX   1, INT32_MAX);
    assert(r == 1);

    r = is_overflow(abs_of_negative_int32(INT32_MIN), INT32_MIN);
    assert(r == 0);

    r = is_overflow(abs_of_negative_int32(INT32_MIN) - 1, INT32_MIN);
    assert(r == -1);

    return 0;
}
  

Комментарии:

1. Хороший обходной путь UB / etc в is_overflow()/abs_of_negative_int32() и тестовый код.

Ответ №3:

Вот как вы можете это сделать:

 uint32 addui(uint32 position, int32 offset, int* overflow)
{
  *overflow = (((offset >= 0) amp;amp; (0xFFFFFFFFu - position < (uint32)offset)) ||
               ((offset < 0) amp;amp; (position < (uint32)-offset)));
  return position   offset;
}
  

Суффикс u предназначен для обеспечения того, чтобы константа 0xFFFFFFFF была беззнакового типа (шестнадцатеричные константы без суффиксов могут быть подписаны или беззнаковыми, в зависимости от значения и того, как ваш компилятор определяет int , long и long long ), и поэтому выражение слева от < является беззнаковым. Возможно, это и не нужно, но я немного устал выяснять, не так ли. Это точно не повредит.

Приведения (uint32) предназначены для того, чтобы заставить компилятор замолчать, который может подумать, что мы делаем что-то глупое (сравнивая signed с unsigned).

ОБНОВЛЕНИЕ: если int32 имеет представление дополнения 2 и смещение = -0x80000000, выражению -offset разрешено вызывать определенный реализацией сигнал или, возможно, даже вызывать неопределенное поведение в соответствии со стандартом C (см. Разделы 6.3.1.3 Signed and unsigned integers и 7.20.6.1 The abs, labs and llabs functions C99), но практически этого никогда не происходит, потому что на большинстве платформ отрицание реализовано как простоеинструкция (или несколько), которая не вызывает никаких исключений / прерываний / ловушек / событий в процессоре, и нет смысла генерировать дополнительный код для проверки этого крайнего случая, тем более, что целые числа представлены в дополнительном коде 2, а абсолютное значение -0x80000000 равно 0x80000000, что может привести кэто удобно (например, для вычисления абсолютных значений). Процессор не очень заботится о целых числах со знаком и даже использует одни и те же инструкции сложения и вычитания для обоих (это преимущество дополнения 2), и он редко заботится о переполнении целых чисел, потому что они происходят регулярно в программном обеспечении и являются образом жизни. Имейте это в виду, но не переживайте.

Пожалуйста, посмотрите, как это реализовано в SafeInt от Microsoft для C (Code, Intro, на MSDN, Video) и IntSafe для C (Intro Code, на MSDN).

Комментарии:

1. Вероятно, это связано с той же проблемой, что и ответ Алекса, то есть -offset может не быть определен, когда offset==INT32_MIN . Это может работать с какой-то конкретной реализацией, но не в целом.