C безопасно принимает абсолютное значение целого числа

Рассмотрим следующую программу (C99):

#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>

int main(void)
{
    printf("Enter int in range %jd .. %jd:\n > ", INTMAX_MIN, INTMAX_MAX);
    intmax_t i;
    if (scanf("%jd", &i) == 1)
        printf("Result: |%jd| = %jd\n", i, imaxabs(i));
}

Теперь, насколько я понимаю, это содержит легко запускаемое неопределенное поведение, например:

Enter int in range -9223372036854775808 .. 9223372036854775807:
 > -9223372036854775808
Result: |-9223372036854775808| = -9223372036854775808

Вопросы:

  1. Действительно ли это неопределенное поведение, как в «коду разрешено запускать любой путь кода, который любой код, который нравится компилятору», когда пользователь вводит неверный номер? Или это какой-то другой аромат не совсем определенного?

  2. Как педантичный программист предохранится от этого, не делая никаких предположений, не гарантированных стандартом?

(Есть несколько связанных вопросов, но я не нашел ответа на вопрос 2 выше, поэтому, если вы предлагаете дубликат, убедитесь, что он отвечает на него.)


person hyde    schedule 07.02.2016    source источник
comment
Обратите внимание, что ввод целого числа за пределами диапазона также приводит к неопределенному поведению. Если вы хотите избежать UB, вы не можете использовать любую разновидность %d или других спецификаторов сканирования с целыми числами или с плавающей запятой. Используйте семейство strto . И есть только один вид неопределенного поведения — плохой.   -  person M.M    schedule 07.02.2016
comment
@M.M Существует также поведение, определяемое реализацией, неуказанное, но допустимое значение и, возможно, некоторые другие более мягкие альтернативы неопределенному поведению. Но я неправильно понимаю, или вы говорите, что scanf для числа со знаком или с плавающей запятой неявно содержит UB, запускаемый пользователем? Ссылка?   -  person hyde    schedule 07.02.2016
comment
Да, пользователь может инициировать UB, введя значение вне допустимого диапазона для сканируемого целого числа. См. спецификацию fscanf в стандарте C. В C11 это 7.21.6.2/10, если результат преобразования не может быть представлен в объекте, поведение не определено. Таким образом, семейство scanf по большей части не подходит для использования в производстве.   -  person M.M    schedule 08.02.2016
comment
Я помню, как много лет назад на моем вводном уроке программирования первым заданием было написать программу для сложения двух чисел, которые могут быть положительными или отрицательными. Я добросовестно написал код, а затем понял, что может быть переполнение и недополнение, поэтому я написал код для обнаружения этого и информирования пользователя, если это произошло. Я полагаю, что подобное можно было бы сделать, чтобы ответить на ваш второй вопрос.   -  person Michael    schedule 08.02.2016


Ответы (7)


Как педантичный программист предохранится от этого, не делая никаких предположений, не гарантированных стандартом?

Один из методов заключается в использовании целых чисел без знака. Поведение переполнения целых чисел без знака четко определено, как и поведение при преобразовании целого числа со знаком в целое число без знака.

Поэтому я думаю, что следующее должно быть безопасным (оказывается, оно ужасно сломано в некоторых действительно малоизвестных системах, см. далее в посте улучшенную версию)

uintmax_t j = i;
if (j > (uintmax_t)INTMAX_MAX) {
  j = -j;
}
printf("Result: |%jd| = %ju\n", i, j);

Так как же это работает?

uintmax_t j = i;

Это преобразует целое число со знаком в беззнаковое. ЕСЛИ оно положительное, значение остается прежним, если отрицательное, значение увеличивается на 2n (где n – количество битов). Это преобразует его в большое число (больше, чем INTMAX_MAX)

if (j > (uintmax_t)INTMAX_MAX) {

Если исходное число было положительным (и, следовательно, меньше или равно INTMAX_MAX), это ничего не делает. Если исходное число было отрицательным, выполняется внутренняя часть блока if.

  j = -j;

Число отрицается. Результат отрицания явно отрицательный и поэтому не может быть представлен как целое число без знака. Таким образом, оно увеличивается на 2n.

Итак, алгебраически результат для отрицательного i выглядит так

j = - (i + 2n) + 2n = -i


Умное, но это решение делает предположения. Это не удается, если INTMAX_MAX == UINTMAX_MAX, что разрешено стандартом C.

Хм, давайте посмотрим на это (я читаю https://busybox.net/~landley/c99-draft.html, который, по-видимому, является последним черновиком C99 до стандартизации, если что-то изменилось в окончательном стандарте, пожалуйста, сообщите мне.

Когда определены имена typedef, отличающиеся только отсутствием или наличием начального u, они должны обозначать соответствующие типы со знаком и без знака, как описано в 6.2.5; реализация не должна предоставлять тип без предоставления соответствующего типа.

В 6.2.5 вижу

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

В 6.2.6.2 вижу

#1

Для целочисленных типов без знака, отличных от unsigned char, биты представления объекта должны быть разделены на две группы: биты значения и биты заполнения (последние не обязательно должны быть). Если имеется N битов значения, каждый бит должен представлять разную степень числа 2 от 1 до 2N-1, так что >объекты этого типа должны быть способны представлять значения от 0 до 2N-1 >используя чисто двоичное представление; это должно быть известно как представление значения. Значения любых битов заполнения не указаны.39)

#2

Для целочисленных типов со знаком биты представления объекта должны быть разделены на три группы: биты значения, биты заполнения и бит знака. Не должно быть битов заполнения; должен быть ровно один бит знака. Каждый бит, являющийся битом значения, должен иметь то же значение, что и тот же бит в объектном представлении соответствующего беззнакового типа (если имеется M битов значения в знаковом типе и N в беззнаковом типе, тогда M‹=N). Если знаковый бит равен нулю, это не должно влиять на результирующее значение.

Так что да, похоже, вы правы, хотя подписанный и неподписанный типы должны быть одинакового размера, кажется, что неподписанный тип имеет на один дополнительный бит больше, чем подписанный тип.


Хорошо, основываясь на приведенном выше анализе, обнаружившем недостаток в моей первой попытке, я написал более параноидальный вариант. Это имеет два изменения по сравнению с моей первой версией.

Я использую i ‹ 0 вместо j > (uintmax_t)INTMAX_MAX для проверки отрицательных чисел. Это означает, что алгоритм выдает правильные результаты для чисел, больших или равных -INTMAX_MAX, даже если INTMAX_MAX == UINTMAX_MAX.

Я добавляю обработку для случая ошибки, когда INTMAX_MAX == UINTMAX_MAX, INTMAX_MIN == -INTMAX_MAX -1 и i == INTMAX_MIN. Это приведет к j=0 внутри условия if, которое мы можем легко проверить.

Из требований стандарта C видно, что INTMAX_MIN не может быть меньше -INTMAX_MAX -1, так как имеется только один бит знака, а количество битов значения должно быть таким же или меньшим, чем в соответствующем беззнаковом типе. Просто не осталось битовых шаблонов для представления меньших чисел.

uintmax_t j = i;
if (i < 0) {
  j = -j;
  if (j == 0) {
    printf("your platform sucks\n");
    exit(1);
  }
}
printf("Result: |%jd| = %ju\n", i, j);

@plugwash Я думаю, что 2501 правильный. Например, значение -UINTMAX_MAX становится равным 1: (-UINTMAX_MAX + (UINTMAX_MAX + 1)), и оно не перехватывается вашим if. - Хайд 58 минут назад

ммм,

предполагая, что INTMAX_MAX == UINTMAX_MAX и i = -INTMAX_MAX

uintmax_t j = i;

после этой команды j = -INTMAX_MAX + (UINTMAX_MAX + 1) = 1

if (i < 0) {

i меньше нуля, поэтому мы запускаем команды внутри if

j = -j;

после этой команды j = -1 + (UINTMAX_MAX + 1) = UINTMAX_MAX

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

person plugwash    schedule 07.02.2016
comment
Я решил принять это, так как на самом деле будет отображаться правильный результат даже для значения INTMAX_MIN. - person hyde; 07.02.2016
comment
Умное, но это решение делает предположения. Это не удается, если INTMAX_MAX == UINTMAX_MAX, что разрешено стандартом C. - person 2501; 07.02.2016
comment
@2501 Возможно ли это? У меня сложилось впечатление, что может быть неправильным, что приведение типа со знаком к соответствующему беззнаковому типу не должно терять биты, и поэтому, если значение со знаком отрицательно, созданное значение без знака должно быть больше, чем максимум со знаком. - person hyde; 07.02.2016
comment
@hyde В параграфе C11 6.2.6.2, p2 говорится, что в целом числе без знака может быть такое же количество битов значения, что и в соответствующем целом числе со знаком (примечание: M‹=N). В этом случае диапазон целого числа со знаком на самом деле больше, потому что целое число со знаком имеет дополнительный знаковый бит, который дает ему отрицательный диапазон. - person 2501; 07.02.2016
comment
@ 2501 В любом случае, это все равно не приведет к неопределенному поведению, а просто к неправильному результату (INTMAX_MAX-1, если я правильно рассчитал биты?), который можно было бы проверить заранее, а иногда просто игнорировать как достаточно близко в зависимости от ситуации. - person hyde; 07.02.2016
comment
Другой вопрос, есть ли процессоры с дополнением 2, которые также имеют INTMAX_MAX == UINTMAX_MAX? (Отмечу, что это выходит за рамки моего вопроса, который специально касается вещей, гарантированных стандартом). - person hyde; 07.02.2016
comment
@hyde 1. Да, просто неправильный результат. 2. Я не знаю ни одного. :), я думаю, что это больше теоретическая проблема. Вы всегда можете добавить #ifdef для этого маловероятного сценария и использовать этот код, если хотите. - person 2501; 07.02.2016
comment
@ 2501 Я добавил более параноидальную версию в конце своего сообщения, включая отчеты об ошибках для случая, когда абсолютное значение INTMAX_MIN не может быть представлено в uintmax_t . Как вам сейчас кажется, или есть еще дыры, которые вы можете найти? - person plugwash; 07.02.2016
comment
@plugwash Да, мои предыдущие три комментария неверны для этих значений, поэтому я удалил их. - person 2501; 07.02.2016
comment
Код в стиле языкового юриста в этом примере является именно тем хрупким, трудным для понимания хитрым кодированием, которого следует избегать!! Есть гораздо более простые, более очевидные правильные решения, которые относятся к конкретному случаю (в данном случае i == INTMAX_MIN). - person Rob11311; 07.02.2016
comment
@ 2501 Я не думаю, что ты прав. Первоначальная версия моего кода основывалась на том, что j=i дает разные результаты для положительных и отрицательных чисел, но более параноидальная версия изменила тест на (i‹0), так что положительные и отрицательные числа больше не должны давать разные значения для j. - person plugwash; 07.02.2016
comment
@plugwash Да, вы правы, я тестировал неправильную версию, и у меня также были неправильно расставленные скобки в моем тесте. Извиняюсь. :-( - person 2501; 08.02.2016

Если результат imaxabs не может быть представлен, это может произойти при использовании дополнения до двух, тогда поведение не определено.

7.8.2.1 Функция imaxabs

  1. Функция imaxabs вычисляет абсолютное значение целого числа j. Если результат не может быть представлен, поведение не определено. 221)

221) Абсолютное значение самого отрицательного числа не может быть представлено в дополнении до двух.

Проверка, которая не делает предположений и всегда определена:

intmax_t i = ... ;
if( i < -INTMAX_MAX )
{
    //handle error
}

(Этот оператор if нельзя использовать, если используется представление дополнения или знака, поэтому компилятор может выдать предупреждение о недоступности кода. Сам код по-прежнему определен и действителен.)

person 2501    schedule 07.02.2016
comment
Спасибо за хорошее решение, это был трудный выбор, но после некоторых размышлений я все же решил принять другой ответ, который показывает, как напечатать правильный результат. - person hyde; 07.02.2016
comment
@hyde За исключением того, что другой ответ не соответствует стандартам, а этот соответствует. - person Voo; 07.02.2016
comment
Гарантируется ли, что -INTMAX_MAX не переполнится? - person nwellnhof; 07.02.2016
comment
@nwellnhof Это гарантировано. См. мой другой комментарий: stackoverflow.com/questions/35251410/ - person 2501; 07.02.2016

В системах с двумя дополнениями получение абсолютного числа самого отрицательного значения действительно является неопределенным поведением, поскольку абсолютное значение будет вне допустимого диапазона. И компилятор ничем не может вам помочь, так как UB происходит во время выполнения.

Единственный способ защититься от этого — сравнить ввод с самым отрицательным значением для типа (INTMAX_MIN в коде, который вы показываете).

person Some programmer dude    schedule 07.02.2016
comment
Это покрывает дополнение до двух (и теряет только одно допустимое число для дополнения до единицы), но я считаю хорошим вопросом, можно ли его обнаружить надежным способом независимо от целочисленного представления (я полагаю, что стандарт не ограничивается только единицей и два дополнения, хотя я должен признать, что никогда не проверял) - person Joachim Isaksson; 07.02.2016
comment
@JoachimIsaksson: стандарт ограничивается одним из трех вариантов: дополнение до двух, дополнение до единицы и величина знака. (C99, 6.2.6.2, параграф 2.) - person Mark Dickinson; 07.02.2016
comment
@JoachimIsaksson if( i < -INTMAX_MAX ) работает для любого представления. Хотя вы можете получить предупреждение компилятора о дополнении и величине знака, поскольку оператор не может быть принят. Я не знаю, как это предотвратить. - person 2501; 07.02.2016
comment
И компилятор ничем не может вам помочь, так как UB происходит во время выполнения. компилятор может создать код, выполняющий проверки во время выполнения ;-) - person coredump; 07.02.2016
comment
Стандарт @skyking не определяет этот тип. Можно вполне стандартно, чтоб было понятнее, что ты имел в виду.? Или еще лучше опубликовать ответ в качестве опровержения на мой. (Мне интересно, возможно я ошибаюсь, но я этого не вижу.) - person 2501; 07.02.2016
comment
@skyking Согласно C, для любого типа со знаком -MAX должен быть представлен: C11 6.2.6.2, p2, потому что целые числа со знаком должны быть одним из этих трех представлений, которые гарантируют эти диапазоны. Для целого числа со знаком невозможно, чтобы его максимальное значение было больше, чем абсолютное минимальное значение. - person 2501; 07.02.2016

Таким образом, вычисление абсолютного значения целого числа вызывает неопределенное поведение в одном единственном случае. На самом деле, хотя неопределенного поведения можно избежать, невозможно дать правильный результат в одном случае.

Теперь рассмотрим умножение целого числа на 3: Здесь у нас гораздо более серьезная проблема. Эта операция вызывает неопределенное поведение в 2/3 всех случаев! А для двух третей всех значений x int найти int со значением 3x просто невозможно. Это гораздо более серьезная проблема, чем проблема абсолютного значения.

person gnasher729    schedule 07.02.2016

Вы можете использовать некоторые битовые хаки:

int v;           // we want to find the absolute value of v
unsigned int r;  // the result goes here 
int const mask = v >> sizeof(int) * CHAR_BIT - 1;

r = (v + mask) ^ mask;

Это хорошо работает, когда INT_MIN < v <= INT_MAX. В случае, когда v == INT_MIN, остается INT_MIN , не вызывая неопределенного поведения.

Вы также можете использовать побитовую операцию, чтобы справиться с этим в системах дополнения и знака.

Ссылка: https://graphics.stanford.edu/~seander/bithacks.html#IntegerAbs

person nalzok    schedule 07.02.2016
comment
Я считаю, что сдвиг вправо целого числа со знаком сам по себе является UB. - person abligh; 07.02.2016
comment
@abligh Если целое число со знаком отрицательное, оно определяется реализацией. Этот ответ также предполагает отсутствие битов заполнения. - person 2501; 07.02.2016
comment
Согласно файлу bit hacks, это решение без ответвлений основано на дополнении 2, но также было запатентовано в США, что также может быть проблемой. - person Rob11311; 07.02.2016

в соответствии с этим http://linux.die.net/man/3/imaxabs

Примечания

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

Чтобы обработать весь диапазон, вы можете добавить что-то подобное в свой код.

    if (i != INTMAX_MIN) {
        printf("Result: |%jd| = %jd\n", i, imaxabs(i));
    } else {  /* Code around undefined abs( INTMAX_MIN) /*
        printf("Result: |%jd| = %jd%jd\n", i, -(i/10), -(i%10));
    }

редактировать: поскольку abs(INTMAX_MIN) не может быть представлено на машине с дополнением до 2, 2 значения в пределах представляемого диапазона объединяются на выходе в виде строки. Протестировано с помощью gcc, хотя для printf требуется %lld, поскольку %jd не поддерживается форматом.

person Ilan Kutsman    schedule 07.02.2016
comment
что такое imax(i+1)+1 и чего он должен достичь? - person Pascal Cuoq; 07.02.2016
comment
Я хотел написать имаксабс, я исправлю. он должен давать правильный результат абсолютного значения INTMAX_MIN. Просто пытаюсь мыслить нестандартно здесь - person Ilan Kutsman; 07.02.2016
comment
imaxbas(i+1)+1 не является обходным путем, он просто добавляет неопределенное поведение во второе дополнение. Фундаментальная причина, по которой imaxabs(INTMAX_MIN) не определена на машине с дополнением до 2, заключается в том, что правильный результат не может быть представлен. Никакое добавление единицы дважды не изменит этого основного факта. - person Pascal Cuoq; 07.02.2016
comment
Хорошо, небольшое изменение: imaxabs(INTMAX_MIN+1) можно представить с помощью машины с дополнением до 2. верно? Теперь вы превращаете его в строку и увеличиваете последний символ перед «\ 0». - person Ilan Kutsman; 07.02.2016
comment
Однако проще использовать div и mod, чтобы поместить INTMAX_MIN в отрицательный диапазон. - person Rob11311; 07.02.2016

  1. Действительно ли это неопределенное поведение, как в «коду разрешено запускать любой путь кода, который любой код, который нравится компилятору», когда пользователь вводит неверный номер? Или это какой-то другой аромат не совсем определенного?

Поведение программы не определено только тогда, когда неверное число успешно введено и передано в imaxabs(), что в типичной системе дополнения до 2 возвращает результат -ve, как вы заметили.

Это неопределенное поведение в данном случае, реализации также будет разрешено завершить программу с ошибкой переполнения, если ALU установит флаги состояния.

Причина «неопределенного поведения» в C заключается в том, что разработчикам компиляторов не нужно защищаться от переполнения, поэтому программы могут работать более эффективно. В то время как в стандарте C для каждой программы C, использующей abs(), пытаться убить вашего первенца, только потому, что вы вызываете ее с слишком большим значением, запись такого кода в объектный файл была бы просто извращенной.

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

r = (i < 0) ? -i : i;
if (r < 0) {   // This code may be pointless
    // Do overflow recovery
    doRecoveryProcessing();
} else {
    printf("%jd", r);
}

Поскольку оптимизатор компилятора может сделать вывод, что отрицательные значения инвертируются, он может, в принципе, определить, что (r ‹0) всегда ложно, поэтому попытка отловить проблему не удалась.

  1. Как педантичный программист предохранится от этого, не делая никаких предположений, не гарантированных стандартом?

Безусловно, лучший способ - просто убедиться, что программа работает в допустимом диапазоне, поэтому в этом случае достаточно проверки ввода (запретить INTMAX_MIN). Программы, печатающие таблицы abs(), должны избегать INT*_MIN и т.д.

    if (i != INTMAX_MIN) {
        printf("Result: |%jd| = %jd\n", i, imaxabs(i));
    } else {  /* Code around undefined abs( INTMAX_MIN) /*
        printf("Result: |%jd| = %jd%jd\n", i, -(i/10), -(i%10));
    }

По-видимому, выписывает абс (INTMAX_MIN) подделкой, позволяя программе выполнить обещание, данное пользователю.

person Rob11311    schedule 07.02.2016