Чи підписано непідписане перетворення на С - це завжди безпечно?


135

Припустимо, у мене є наступний код C.

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

unsigned int result = u + i;

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

Відповіді:


223

Коротка відповідь

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

Довга відповідь

Відповідно до стандарту C99:

6.3.1.8 Звичайні арифметичні перетворення

  1. Якщо обидва операнди мають один і той же тип, подальше перетворення не потрібно.
  2. В іншому випадку, якщо обидва операнди мають підписані цілі типи або обидва мають непідписані цілочисельні типи, операнд із типом меншого рангу перетворення цілого числа перетворюється на тип операнда з більшим рангом.
  3. В іншому випадку, якщо операнд, який має непідписаний цілочисельний тип, має ранг більше або дорівнює рангу типу іншого операнда, то операнд з підписаним цілим типом перетворюється на тип операнда з непідписаним цілим числом.
  4. В іншому випадку, якщо тип операнда з підписаним цілим числом може представляти всі значення типу операнда з непідписаним цілим типом, то операнд з непідписаним цілим типом перетворюється на тип операнда з підписаним цілим числом.
  5. В іншому випадку обидва операнди перетворюються на непідписаний цілочисельний тип, відповідний типу операнду з підписаним цілим числом.

У вашому випадку у нас є один неподписаний int ( u) та підписаний int ( i). Посилаючись на (3) вище, оскільки обидва операнди мають один і той самий ранг, вам iпотрібно буде перетворити на непідписане ціле число.

6.3.1.3 Підписані та непідписані цілі числа

  1. Коли значення з цілим типом перетворюється на інший цілий тип, відмінний від _Bool, якщо значення може бути представлено новим типом, воно не змінюється.
  2. В іншому випадку, якщо новий тип не підписаний, значення перетворюється багаторазовим додаванням або відніманням на одне більше, ніж максимальне значення, яке може бути представлене в новому типі, поки значення не буде в діапазоні нового типу.
  3. В іншому випадку новий тип підписаний, і значення не може бути представлено в ньому; або результат визначений реалізацією, або підвищений сигнал, визначений реалізацією.

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

6.2.5 (9)

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

Бонус: Арифметична конверсія Semi-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;
}

Ви можете скористатися цим посиланням, щоб спробувати це в Інтернеті: https://repl.it/repls/QuickWhimsicalBytes

Бонус: Побічний ефект арифметичної конверсії

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

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

Це гарантовано є портативним незалежно від підписаного номера системи в системі, оскільки описані вище правила перетворення. Дивіться це питання ТА для отримання додаткової інформації: Чи безпечно використовувати -1, щоб встановити всі біти на істинні?


Я не розумію, чому він не може просто зробити абсолютне значення, а потім трактувати це так само, як і з позитивними цифрами?
Хосе Сальватьєра

7
@ D.Singh, чи можна вказувати на неправильні частини відповіді?
Шміль Кіт

Для перетворення підписаного в безпідписане ми додаємо максимальне значення неподписаного значення (UINT_MAX +1). Аналогічно, який найпростіший спосіб перейти з непідписаного до підписаного? Чи потрібно нам відняти задане число від максимального значення (256 у випадку неподписаного знака)? Наприклад: 140 при перетворенні на підписаний номер стає -116. Але 20 стає самим 20. Тож якийсь простий трюк тут?
Джон Уілок


24

Перетворення від підписаного до непідписаного не обов'язково просто копіювати або переосмислювати подання підписаного значення. Цитуючи стандарт C (C99 6.3.1.3):

Коли значення з цілим типом перетворюється на інший цілий тип, відмінний від _Bool, якщо значення можна представити новим типом, воно не змінюється.

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

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

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

Взагалі, перетворення в C визначені для роботи на значеннях, а не на поданнях.

Щоб відповісти на початкове запитання:

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

unsigned int result = u + i;

Значення i перетворюється на непідписаний int, поступаючись UINT_MAX + 1 - 5678. Потім це значення додається до непідписаного значення 1234, отримуючи результат UINT_MAX + 1 - 4444.

(На відміну від неподписаного переповнення, підписаний переповнення викликає невизначене поведінку. Обертання є загальним, але це не гарантується стандартом C - і оптимізація компілятора може призвести до хаосу коду, який робить необґрунтовані припущення.)


5

Посилаючись на Біблію :

  • Ваша операція додавання спричиняє перетворення int на безпідписаний int.
  • Якщо припустити представлення комплексу двох та типи однакового розміру, бітова структура не змінюється.
  • Перетворення з непідписаного int до підписаного int залежить від реалізації. (Але це, мабуть, працює так, як ви очікуєте на більшості платформ сьогодні.)
  • Правила є трохи складнішими у випадку поєднання підписаних та безпідписаних різних розмірів.

3

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

Тож це безпечно в тому сенсі, що результат може бути величезним і неправильним, але він ніколи не зірветься.


Неправда. 6.3.1.8 Звичайні арифметичні перетворення Якщо підсумовувати int та неподписаний знак, то останній перетворюється на int. Якщо підсумовувати два неподписані знаки, вони перетворюються на int.
2501 р

3

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


1

Як було сказано раніше, ви можете без проблем переходити між підписаними та неподписаними. Кордон для підписаних цілих чисел дорівнює -1 (0xFFFFFFFF). Спробуйте додавати і віднімати з цього, і ви побачите, що ви можете відкинути назад і вважати це правильним.

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

int iValue, iResult;
unsigned int uValue, uResult;

Занадто просто відволіктися на більш важливі проблеми та забути, яка змінна є типом, якщо вони названі без натяку. Ви не хочете, щоб передати їх без підпису, а потім використовувати це як індекс масиву.


0

Які неявні конверсії відбуваються тут,

я перетворяться на непідписане ціле число.

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

Безпечний у сенсі чітко визначеного так (див. Https://stackoverflow.com/a/50632/5083516 ).

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

Додавання, віднімання та множення працюватимуть правильно на цих числах, у результаті виходить інше непідписане ціле число, що містить число доповнення двійки, що представляє "реальний результат".

поділ і виведення на більші цілі цілі цілі без підписання матимуть чітко визначені результати, але ці результати не будуть представленням 2-го доповнення "реального результату".

(Безпечно, в тому сенсі, що, хоча результат у цьому прикладі переповниться якоюсь величезною позитивною кількістю, я можу повернути його до int і отримати реальний результат.)

У той час як конверсії від підписаного до непідписаного визначаються стандартом, зворотне визначається реалізацією, і gcc, і msvc визначають конверсію таким чином, що ви отримаєте "реальний результат" при перетворенні 2-го доповнення, збереженого в непідписане ціле число, до підписаного цілого числа . Я сподіваюся, що ви знайдете лише будь-яку іншу поведінку в незрозумілих системах, які не використовують доповнення 2 для підписаних цілих чисел.

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


-17

Жахливі відповіді Галор

Ozgur Ozcitak

Якщо ви переходите від підписаного до непідписаного (і навпаки), внутрішнє представлення номера не змінюється. Що змінюється, як компілятор інтерпретує біт знаків.

Це абсолютно неправильно.

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

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

Це теж неправильно. Непідписані вкладиші можуть бути переведені в ints, якщо вони мають однакову точність через біт підкладки в неподписаному типі.

смх

Ваша операція додавання спричиняє перетворення int на безпідписаний int.

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

Перетворення з непідписаного int до підписаного int залежить від реалізації. (Але це, мабуть, працює так, як ви очікуєте на більшості платформ сьогодні.)

Неправильно. Це або невизначена поведінка, якщо вона викликає переповнення, або значення зберігається.

Анонімний

Значення i перетворюється на непідписаний int ...

Неправильно. Залежить від точності int щодо непідписаного int.

Тейлор Прайс

Як було сказано раніше, ви можете без проблем переходити між підписаними та неподписаними.

Неправильно. Спроба зберігати значення поза діапазоном підписаного цілого числа призводить до невизначеної поведінки.

Тепер я можу нарешті відповісти на питання.

Якщо точність int дорівнює неподписаному int, u буде підвищено до підписаного int, і ви отримаєте значення -4444 з виразу (u + i). Тепер, якщо у і у мене є інші значення, ви можете отримати переповнення та невизначене поведінку, але при таких точних числах ви отримаєте -4444 [1] . Це значення матиме тип int. Але ви намагаєтесь зберегти це значення в безпідписаний int, щоб потім було передано неподписаний int, і значення, яке отримає результат, було б (UINT_MAX + 1) - 4444.

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

Що таке точність?

Цілі особи мають біти для замітів, біти знаків та біти значень. У безпідписаних цілих чисел очевидно немає бітових знаків. Далі гарантовано, що непідписаний графік не має шматочків оббивки. Кількість бітів значень, яке має ціле число, - скільки точності має.

[Gotchas]

Сам макрос розміру макросу не може бути використаний для визначення точності цілого числа, якщо наявні біти для заміщення. А розмір байта не повинен бути октетом (вісім біт), як визначено у C99.

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


6
"Непідписані вставки можуть бути переведені до ints". Неправда. Не відбувається ціле просування, оскільки типи вже є рангом> = int. 6.3.1.1: "Ранг будь-якого цілого цілого числа без підпису повинен дорівнювати рангу відповідного підписаного цілого типу, якщо такий є". та 6.3.1.8: "В іншому випадку, якщо операнд, який має непідписаний цілочисельний тип, має ранг більше або дорівнює рангу типу іншого операнда, то операнд з підписаним цілим типом перетворюється на тип операнда з непідписаним цілим числом тип. " обидві гарантії, на які intперетворюється, unsigned intколи застосовуються звичайні арифметичні перетворення.
CB Bailey

1
6.3.1.8 Виникає лише після цілого просування. У вступному пункті сказано: "В іншому випадку цілі промоції виконуються на обох операндах. ТОГО до рекламованих операндів застосовуються наступні правила". Отож, прочитайте правила просування 6.3.1.1 ... "Об'єкт чи вираз із цілим типом, чий цілочисельний ранг перетворення менший або EQUAL до рангу int та неподписаного int" та "Якщо int може представляти всі значення оригінального типу, значення перетворюється на int ".
Elite Mx

1
6.3.1.1 Просування цілого числа, яке використовується для перетворення деяких цілих типів, які не є, intабо unsigned intдо одного з тих типів, де щось типу unsigned intабо intочікується. У TC2 додано "або рівне", щоб дозволити переліченим типам ранжування конверсії, рівним intабо unsigned intперетвореному в один із цих типів. Ніколи не передбачалося, щоб описана акція конвертувала між unsigned intта int. Загальне визначення типу між unsigned intі intдосі регулюється 6.3.1.8, навіть після TC2.
CB Bailey

19
Публікація неправильних відповідей, критикуючи неправильні відповіді інших, не здається гарною стратегією для роботи ... ;-)
R .. GitHub ЗАСТОСУЙТЕ ДОПОМОГУ

6
Я не голосую за видалення, оскільки цей рівень помилок у поєднанні із зарозумілістю занадто захоплюючий
ММ
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.