Чи визначено поведінку без підписаного цілого віднімання?


100

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

unsigned int To, Tf;

To = getcounter();
while (1) {
    Tf = getcounter();
    if ((Tf-To) >= TIME_LIMIT) {
        break;
    } 
}

Це єдина нечітко відповідна цитата зі стандарту С, яку я міг знайти.

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

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

тобто

0x0000 - 0x0001 == 0x 1 0000 - 0x0001 == 0xFFFF

на відміну від використання підписаної семантики, залежної від реалізації:

0x0000 - 0x0001 == (без підпису) (0 + -1) == (0xFFFF, але також 0xFFFE або 0x8001)

Яке чи яке тлумачення є правильним? Чи визначено це взагалі?


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

Відповіді:


107

Результат віднімання, що генерує від'ємне число у неподписаному типі, є чітко визначеним:

  1. [...] Обчислення, що включають непідписані операнди, ніколи не можуть переповнюватись, оскільки результат, який не може бути представлений результатом непідписаного цілого типу, зменшується за модулем на число, яке на одну величину більше, ніж найбільше значення, яке може бути представлено отриманим типом. (ISO / IEC 9899: 1999 (E) § 6.2.5 / 9)

Як бачимо, (unsigned)0 - (unsigned)1дорівнює -1 модуль UINT_MAX + 1, або іншими словами, UINT_MAX.

Зауважте, що хоч це говорить: "Обчислення, що включають непідписані операнди, ніколи не можуть переповнювати", що може привести вас до думки, що воно застосовується лише для перевищення верхньої межі, це подається як мотивація фактичної обов'язкової частини речення: "a Результат, який не може бути представлений отриманим неподписаним цілим типом, зменшується по модулю на число, яке на одне більше, ніж найбільше значення, яке може бути представлено отриманим типом. " Ця фраза не обмежується переповненням верхньої межі типу і застосовується однаково до значень, занадто низьких, щоб бути представленими.


2
Дякую! Зараз я бачу тлумачення, яке мені не вистачало. Я думаю, що вони могли обрати більш чітке формулювання.

4
Зараз я відчуваю себе набагато краще, знаючи, що якщо будь-яке непідписане додавання котиться до нуля і спричиняє хаос, це буде тому, що uintзавжди було призначено представляти математичне кільце цілих чисел 0через UINT_MAXоперації додавання та множення модуля UINT_MAX+1, а не тому, що переливу. Однак виникає питання, чому, якщо кільця є таким основним типом даних, мова не пропонує більш загальної підтримки для кілець інших розмірів.
Теодор Мердок

2
@TheodoreMurdock Я думаю, що відповідь на це питання проста. Наскільки я можу сказати, той факт, що це кільце - це наслідок, а не причина. Справжня вимога полягає в тому, що неподписані типи повинні брати участь у всіх своїх бітах у представленні значення. Кільцеподібна поведінка випливає природно з цього. Якщо ви бажаєте такої поведінки інших типів, то виконайте свою арифметику з подальшим застосуванням необхідного модуля; що використовує основні оператори.
підкреслюй_

@underscore_d Звичайно ... зрозуміло, чому вони прийняли дизайнерське рішення. Це просто дивно, що вони написали специфікацію грубо, оскільки "немає арифметичного переповнення / підтоплення, тому що тип даних є специфічним як кільце", як ніби такий вибір дизайну означав, що програмістам не доведеться обережно уникати надмірного і нижчого -потоку або їх програми вражають невдало.
Теодор Мердок

120

Коли ви працюєте з непідписаними типами, відбувається модульна арифметика (також відома як поведінка "обернути навколо" ). Щоб зрозуміти цю модульну арифметику , просто подивіться на ці години:

введіть тут опис зображення

9 + 4 = 1 ( 13 мод 12 ), тому в інший бік це: 1 - 4 = 9 ( -3 mod 12 ). Цей же принцип застосовується під час роботи з неподписаними типами. Якщо тип результату є unsigned, то відбувається модульна арифметика.


Тепер подивіться на наступні операції, що зберігають результат як unsigned int:

unsigned int five = 5, seven = 7;
unsigned int a = five - seven;      // a = (-2 % 2^32) = 4294967294 

int one = 1, six = 6;
unsigned int b = one - six;         // b = (-5 % 2^32) = 4294967291

Коли ви хочете переконатися в тому, що результат є signed, тоді зберігайте його в signedзмінній або передайте його signed. Якщо ви хочете отримати різницю між числами і переконатися, що модульна арифметика не буде застосована, то слід розглянути можливість використання abs()функції, визначеної у stdlib.h:

int c = five - seven;       // c = -2
int d = abs(five - seven);  // d =  2

Будьте дуже обережні, особливо під час написання, тому що:

if (abs(five - seven) < seven)  // = if (2 < 7)
    // ...

if (five - seven < -1)          // = if (-2 < -1)
    // ...

if (one - six < 1)              // = if (-5 < 1)
    // ...

if ((int)(five - seven) < 1)    // = if (-2 < 1)
    // ...

але

if (five - seven < 1)   // = if ((unsigned int)-2 < 1) = if (4294967294 < 1)
    // ...

if (one - six < five)   // = if ((unsigned int)-5 < 5) = if (4294967291 < 5)
    // ...

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

5
@LightnessRacesinOrbit: Дякую Я написав це, бо думаю, що хтось може вважати це дуже корисним. Я згоден, що це не повна відповідь.
LihO

4
Лінія int d = abs(five - seven);- це не добре. Спочатку five - sevenобчислюється: просування залишає типи операндів як unsigned int, результат обчислюється модулем (UINT_MAX+1)і оцінюється до UINT_MAX-1. Тоді це значення є фактичним параметром abs, що є поганою новиною. abs(int)викликає не визначене поведінку, передаючи аргумент, оскільки воно не знаходиться в діапазоні, і, abs(long long)ймовірно, може утримувати значення, але невизначене поведінка виникає, коли повернене значення примушується intініціалізувати d.
Бен Войгт

1
@LihO: Єдиний оператор в C ++, який є залежним від контексту і діє по-різному в залежності від того, як використовується його результат, - це спеціальний оператор перетворення operator T(). Додавання у двох обговорених нами виразах виконується на тип unsigned int, заснований на типах операндів. Результатом додавання є unsigned int. Тоді цей результат неявно перетворюється на тип, необхідний у контексті, перетворення якого не вдається, оскільки значення не є представним у новому типі.
Бен Войгт

1
@LihO: Це може допомогти подумати double x = 2/3;протиdouble y = 2.0/3;
Бен Войгт

5

Ну, перше тлумачення правильне. Однак Ваші міркування про "підписану семантику" в цьому контексті помилкові.

Знову ж таки, ваше перше тлумачення правильне. Беззнаковий арифметична слідувати правилам арифметики по модулю, а це означає , що має 0x0000 - 0x0001значення 0xFFFFдля 32-бітних беззнакових типів.

Однак друге тлумачення (те, яке ґрунтується на «підписаній семантиці») також вимагає того ж результату. Тобто, навіть якщо ви оцінюєте 0 - 1домен підписаного типу і отримуєте -1як проміжний результат, це -1все одно потрібно виробляти, 0xFFFFколи пізніше він переходить у неподписаний тип. Навіть якщо деяка платформа використовує екзотичне подання для підписаних цілих чисел (доповнення 1, величина з підписами), ця платформа все одно зобов'язана застосовувати правила модульної арифметики під час перетворення підписаних цілих цілих чисел у неподписані цілі.

Наприклад, це оцінювання

signed int a = 0, b = 1;
unsigned int c = a - b;

по - , як і раніше гарантовано виробляти UINT_MAXв c, навіть якщо платформа використовує екзотичне уявлення для цілих чисел.


4
Я думаю, ви маєте на увазі 16-бітові неподписані типи, а не 32-бітні.
xioxox

4

З безпідписаними числами типу unsigned intабо більше, за відсутності перетворень типу, a-bвизначається як отримання неподписаного числа, яке при додаванні bдасть результат a. Перетворення від'ємного числа в непідписане визначається як отримання числа, яке при додаванні до знака, оберненого знаком, поверне нулю (тому перетворення -5 в неподписане дасть значення, яке при додаванні до 5 приведе до нуля) .

Зауважте, що безпідписані числа менші, ніж unsigned intможуть бути запропоновані до введення intдо віднімання, поведінка a-bбуде залежати від розміру int.

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