Подписано на неподписанное преобразование в C - всегда ли оно безопасно?

Предположим, что у меня есть следующий код C.

unsigned int u = 1234;
int i = -5678;

unsigned int result = u + i;

Какие неявные преобразования происходят здесь, и этот код безопасен для всех значений u и i? (Безопасный, в том смысле, что даже при том, что результат этого примера переполнится до некоторого огромного положительного числа, я мог бы вернуть его обратно в int и получить реальный результат.)

Ответ 1

Короткий ответ

Ваш i будет преобразован в целое число без знака, добавив UINT_MAX + 1, тогда добавление будет выполняться с неподписанными значениями, что приведет к большому result (в зависимости от значений u и i).

Длинный ответ

В соответствии со стандартом C99:

6.3.1.8 Обычные арифметические преобразования

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

В вашем случае у нас есть один unsigned int (u) и подписанный int (i). Ссылаясь на (3) выше, поскольку оба операнда имеют один и тот же ранг, ваш i должен быть преобразован в целое число без знака.

6.3.1.3 Целочисленные и беззнаковые целые числа

  • Когда значение с целым типом преобразуется в другой целочисленный тип, отличный от _Bool, если значение может быть представлено новым типом, оно не изменяется.
  • В противном случае, если новый тип без знака, значение преобразуется путем многократного добавления или вычитания более чем максимального значения, которое может быть представлено в новом типе, пока значение не будет в диапазоне нового типа.
  • В противном случае новый тип подписан и значение не может быть представлено в нем; либо результат определяется реализацией, либо генерируется сигнал, определяемый реализацией.

Теперь нам нужно обратиться к (2) выше. Ваш i будет преобразован в значение без знака, добавив UINT_MAX + 1. Таким образом, результат будет зависеть от того, как UINT_MAX определяется для вашей реализации. Он будет большим, но он не будет переполняться, потому что:

6.2.5 (9)

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

Бонус: Арифметическое преобразование Полу-WTF

#include <stdio.h>

int main(void)
{
  unsigned int plus_one = 1;
  int minus_one = -1;

  if(plus_one < minus_one)
    printf("1 < -1");
  else
    printf("boring");

  return 0;
}

Вы можете воспользоваться этой ссылкой: http://codepad.org/yPhYCMFO

Бонус: побочный эффект арифметической конверсии

Арифметические правила преобразования могут использоваться для получения значения UINT_MAX путем инициализации значения без знака до -1, то есть:

unsigned int umax = -1; // umax set to UINT_MAX

Гарантируется, что он будет переносимым, независимо от подписанного числа в системе, из-за описанных выше правил преобразования. См. Этот вопрос SO для получения дополнительной информации: Можно ли использовать -1 для установки всех битов в true?

Ответ 2

Преобразование из подписанного в unsigned не позволяет не копировать или переинтерпретировать представление знакового значения. Цитирование стандарта C (C99 6.3.1.3):

Когда значение с целым типом преобразуется в другой целочисленный тип, отличный от _Bool, if значение может быть представлено новым типом, оно не изменяется.

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

В противном случае новый тип подписан и значение не может быть представлено в нем; либо результат определяется реализацией или вырабатывается определенный реализацией сигнал.

Для двух дополняющих представлений, которые почти универсальны в наши дни, правила соответствуют переинтерпретации бит. Но для других представлений (дополнение знака и величины или единицы) реализация C должна по-прежнему обеспечивать одинаковый результат, а это означает, что преобразование не может просто скопировать бит. Например, (без знака) -1 == UINT_MAX, независимо от представления.

В общем, преобразования в C определены для работы с значениями, а не с представлениями.

Чтобы ответить на исходный вопрос:

unsigned int u = 1234;
int i = -5678;

unsigned int result = u + i;

Значение я преобразуется в unsigned int, что дает UINT_MAX + 1 - 5678. Затем это значение добавляется к значению без знака 1234, что дает UINT_MAX + 1 - 4444.

(В отличие от неподписанного переполнения, подписанное переполнение вызывает поведение undefined. Обходное соединение является общим, но не гарантируется стандартом C, а оптимизация компилятора может привести к хаосу кода, который делает необоснованные допущения.)

Ответ 3

Когда одна беззнаковая и одна подписанная переменная добавляются (или любая двоичная операция), оба неявно преобразуются в unsigned, что в этом случае приведет к огромному результату.

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

Ответ 4

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

Ответ 5

Ссылаясь на библию:

  • Ваша операция добавления приводит к тому, что int преобразуется в unsigned int.
  • Если предположить, что два представления представления и типы одинакового размера, бит-шаблон не изменяется.
  • Преобразование из unsigned int в подписанный int зависит от реализации. (Но он, вероятно, работает так, как вы ожидаете на большинстве платформ в эти дни.)
  • Правила немного сложнее в случае объединения подписанных и неподписанных разного размера.

Ответ 6

Как уже сообщалось ранее, вы можете выполнять резервное копирование между подписанным и неподписанным без проблем. Граничный случай для целых чисел со знаком равен -1 (0xFFFFFFFF). Попробуйте добавить и вычесть из этого, и вы обнаружите, что можете отбросить назад и сделать это правильно.

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

int iValue, iResult;
unsigned int uValue, uResult;

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

Ответ 7

Какие неявные преобразования здесь происходят,

i будет преобразован в целое число без знака.

и этот код безопасен для всех значений u и i?

Безопасно в смысле четкости да (см. fooobar.com/questions/9365/...).

Правила написаны, как правило, с трудными для чтения стандартами - говорят, но по существу независимо от того, какое представление было использовано в значении целого числа, целое число без знака будет содержать 2-символьное представление числа.

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

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

(Безопасно, в том смысле, что даже при том, что результат этого примера переполнится до некоторого огромного положительного числа, я мог бы вернуть его обратно в int и получить реальный результат.)

В то время как преобразования из подписанного в unsigned определяются стандартом, реверс определяется реализацией как gcc, так и msvc, определяют преобразование таким образом, что вы получите "реальный результат" при преобразовании второго номера дополнения, сохраненного в целое число без знака, назад к целое число со знаком. Я ожидаю, что вы найдете только другое поведение в неясных системах, которые не используют 2 дополнения для целых чисел со знаком.

https://gcc.gnu.org/onlinedocs/gcc/Integers-implementation.html#Integers-implementation https://msdn.microsoft.com/en-us/library/0eex498h.aspx

Ответ 8

Ужасные ответы Галор

Озгур Озцитак

Когда вы отбрасываете из подписанного без знака (и наоборот) внутреннего представление числа не изменение. Какие изменения компилятор интерпретирует бит знака.

Это совершенно неправильно.

Матс Фредрикссон

Когда один знак и один подписали переменная добавляется (или любая бинарная ) неявно преобразуется в unsigned, что этот случай приводит к огромному результату.

Это тоже неправильно. Unsigned ints можно повысить до ints, если они имеют равную точность из-за заполнения битов в неподписанном типе.

SMH

Ваша операция добавления вызывает int для преобразования в unsigned int.

Неправильно. Может быть, и так, а может, и нет.

Преобразование из unsigned int в подписанное int зависит от реализации. (Но он, вероятно, работает так, как вы ожидаете на большинстве платформ в эти дни.)

Неправильно. Это либо поведение undefined, если оно вызывает переполнение, либо значение сохраняется.

Anonymous

Значение я преобразуется в unsigned int...

Неправильно. Зависит от точности int относительно unsigned int.

Стоимость Тейлора

Как уже было сказано, вы можете отбрасывать назад и вперед между подписанными и без знака без проблем.

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

Теперь я могу, наконец, ответить на вопрос.

Если точность int будет равна unsigned int, u будет продвигаться до подписанного int, и вы получите значение -4444 из выражения (u + i). Теперь, если у и у меня есть другие значения, вы можете получить переполнение и undefined поведение, но с теми точными номерами вы получите -4444 [1]. Это значение будет иметь тип int. Но вы пытаетесь сохранить это значение в unsigned int, чтобы затем было перенесено на unsigned int, и значение, которое результат будет в конечном итоге иметь (UINT_MAX + 1) - 4444.

Если точность unsigned int больше, чем значение int, подписанный int будет продвигаться в unsigned int, давая значение (UINT_MAX + 1) - 5678, которое будет добавлено к другому unsigned int 1234. Если u и у меня есть другие значения, которые вытесняют выражение вне диапазона {0..UINT_MAX}, значение (UINT_MAX + 1) будет либо добавлено, либо вычитано до тех пор, пока результат не попадет внутрь диапазона {0..UINT_MAX) и не будет undefined.

Что такое точность?

Целые имеют биты заполнения, биты знака и биты значений. Очевидно, что целые числа без знака не имеют знакового бита. Кроме того, без знака char не будет битов заполнения. Число битов значений, число которых имеет целое число, равно степени точности.

[Gotchas]

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

[1] Переполнение может происходить в одной из двух точек. Либо перед добавлением (во время продвижения) - когда у вас есть unsigned int, который слишком велик, чтобы помещаться внутри int. Переполнение может также произойти после добавления, даже если неподписанный int находился в пределах интервала int, после добавления результат все еще может переполняться.


В отношении несвязанной ноты я - недавний аспирант, пытающийся найти работу;)