У деяких відповідях тут згадуються дивовижні правила просування між підписаними та беззнаковими значеннями, але це, схоже, більше нагадує проблему, пов’язану зі змішуванням знакових та беззнакових значень, і не обов’язково пояснює, чому підписані змінні мають перевагу над беззнаковими поза сценаріями змішування.
На моєму досвіді, поза змішаними порівняннями та правилами просування, є дві основні причини, чому беззнакові значення є магнітами помилок наступним чином.
Беззнакові значення мають розрив у нулі, найпоширеніше значення в програмуванні
Як цілі числа без знака, так і зі знаком мають a розриви у мінімальних і максимальних значеннях, де вони обертаються (без знака) або спричиняють невизначену поведінку (підписано). Для unsigned
них точки знаходяться на нулі і UINT_MAX
. Бо int
вони на INT_MIN
і INT_MAX
. Типовими значеннями системи INT_MIN
та INT_MAX
в системі з 4-байтовими int
значеннями є -2^31
і 2^31-1
, і для такої системи UINT_MAX
зазвичай 2^32-1
.
Основна проблема, unsigned
що спричиняє помилку, і яка не стосується цього, int
полягає в тому, що вона має розрив у нулі . Нуль, звичайно, є дуже поширеним значенням у програмах, поряд з іншими малими значеннями, такими як 1,2,3. Загальноприйнято додавати і віднімати невеликі значення, особливо 1, у різних конструкціях, і якщо ви віднімаєте що-небудь зі unsigned
значення, і воно буває нульовим, ви просто отримали величезне позитивне значення та майже певну помилку.
Розглянемо ітерацію коду над усіма значеннями у векторі за індексом, крім останніх 0,5 :
for (size_t i = 0; i < v.size() - 1; i++) {
Це чудово працює, поки одного разу ви не передасте порожній вектор. Замість того, щоб робити нульові ітерації, ви отримуєте v.size() - 1 == a giant number
1, а ви зробите 4 мільярди ітерацій і майже матимете вразливість до переповнення буфера.
Вам потрібно написати це так:
for (size_t i = 0; i + 1 < v.size(); i++) {
Тож це можна "виправити" у цьому випадку, але лише ретельно продумавши непозначений характер size_t
. Іноді ви не можете застосувати вказане вище виправлення, оскільки замість постійного у вас є якийсь змінний зсув, який ви хочете застосувати, який може бути позитивним чи негативним: отже, на якій «стороні» порівняння вам потрібно поставити його, залежить від підписаності - тепер код стає дуже брудним.
Існує подібна проблема з кодом, який намагається виконати ітерацію до нуля включно. Щось на зразок while (index-- > 0)
чудово працює, але, мабуть, еквівалент while (--index >= 0)
ніколи не закінчується для беззнакового значення. Ваш компілятор може попередити вас , коли права рука буквальним дорівнює нулю, але , звичайно , немає , якщо це значення визначається під час виконання.
Контрапункт
Деякі можуть стверджувати, що підписані значення також мають дві розриви, то навіщо обирати непідписані? Різниця полягає в тому, що обидва розриви дуже (максимально) далекі від нуля. Я справді вважаю це окремою проблемою "переповнення", як знакові, так і непідписані можуть переповнюватися при дуже великих значеннях. У багатьох випадках переповнення неможливе через обмеження можливого діапазону значень, а переповнення багатьох 64-розрядних значень може бути фізично неможливим). Навіть якщо це можливо, ймовірність помилки, пов’язаної з переповненням, часто є незначною в порівнянні з помилкою «при нулі», і переповнення трапляється і для непідписаних значень . Отже, непідписаний поєднує в собі найгірше з обох світів: потенційно перелив із дуже великими значеннями величини та розрив у нулі. Підписано лише перше.
Багато хто буде сперечатися "ти трохи програєш" з непідписаними. Це часто правда - але не завжди (якщо вам потрібно представити різницю між беззнаковими значеннями, ви все одно втратите цей біт: так багато 32-бітових речей у будь-якому випадку обмежуються 2 ГіБ, або у вас буде дивна сіра зона, де кажуть: файл може бути розміром 4 Гб, але не можна використовувати певні API на другій половині 2 Гб).
Навіть у тих випадках, коли неподписаний купує вас трохи: він не купує вам багато: якщо вам довелося підтримати більше 2 мільярдів "речей", вам, ймовірно, незабаром доведеться підтримати більше 4 мільярдів.
Логічно, що непідписані значення - це підмножина підписаних значень
Математично непідписані значення (невід’ємні цілі числа) є підмножиною підписаних цілих чисел (просто називаються _integers). 2 . Проте підписані значення природним чином вискакують із операцій виключно над беззнаковими значеннями, такими як віднімання. Можна сказати, що непідписані значення не закриваються відніманням. Те саме не стосується підписаних значень.
Хочете знайти "дельту" між двома непідписаними індексами у файлі? Ну, краще відніміть у правильному порядку, інакше ви отримаєте неправильну відповідь. Звичайно, вам часто потрібна перевірка виконання, щоб визначити правильний порядок! Маючи справу з безпідписаними значеннями як числами, ви часто виявляєте, що (логічно) підписані значення все одно постійно відображаються, тому ви можете також почати з підписаного.
Контрапункт
Як зазначалося у виносці (2) вище, підписані значення в C ++ насправді не є підмножиною непідписаних значень однакового розміру, тому безпідписані значення можуть представляти однакову кількість результатів, яку можуть підписані значення.
Правда, але асортимент менш корисний. Розглянемо віднімання та числа без знака з діапазоном від 0 до 2N та числа зі знаком із діапазоном від -N до N. Довільні віднімання призводять до результатів у діапазоні від -2N до 2N в обох випадках, і цілий тип цілого числа може представляти лише його половина. Ну виявляється, що область, зосереджена навколо нуля від -N до N, зазвичай набагато корисніша (містить більше фактичних результатів у коді реального світу), ніж діапазон від 0 до 2N. Розглянемо будь-який типовий розподіл, відмінний від рівномірного (журнал, zipfian, нормальний, будь-який інший), і розглянемо віднімання випадково вибраних значень із цього розподілу: таким чином більше значень закінчується в [-N, N], ніж [0, 2N] (насправді, результуючий розподіл завжди відцентрований на нулі).
64-біт закриває двері з багатьох причин використовувати підписані значення як числа
Я думаю, що наведені вище аргументи вже були переконливими для 32-розрядних значень, але випадки переповнення, які впливають як на підписані, так і на непідписані з різними порогами, мають місце для 32-розрядних значень, оскільки "2 мільярди" - це число, яке може перевищувати багато абстрактні та фізичні величини (мільярди доларів, мільярди наносекунд, масиви з мільярдами елементів). Отже, якщо когось досить переконає подвоєння позитивного діапазону для беззнакових значень, він може зробити випадок, що переповнення має значення, і це трохи надає перевагу беззнаковому.
Поза спеціалізованими доменами 64-розрядні значення значною мірою усувають цю проблему. Підписані 64-розрядні значення мають верхній діапазон 9 223 372 036 854 775 807 - понад дев'ять квінтільйонів . Це багато наносекунд (близько 292 років) і багато грошей. Це також більший масив, ніж будь-який комп’ютер, імовірно, матиме оперативну пам’ять у зв’язному адресному просторі протягом тривалого часу. То, можливо, 9 квінтильйонів цілком достатньо всім (на даний момент)?
Коли використовувати беззнакові значення
Зверніть увагу, що керівництво стилем не забороняє або навіть не обов'язково відмовляє від використання непідписаних номерів. Він завершується:
Не використовуйте беззнаковий тип лише для того, щоб стверджувати, що змінна не є від’ємною.
Дійсно, є хороші способи використання змінних без знака:
Коли ви хочете обробляти N-бітову величину не як ціле число, а просто як "мішок бітів". Наприклад, як бітова маска або растрове зображення, або N булевих значень, або що завгодно. Це використання часто йде рука об руку з типами фіксованої ширини, як-от uint32_t
і uint64_t
оскільки ви часто хочете знати точний розмір змінної. Підказка , що конкретна змінна заслуговує на це лікування , що ви працювати тільки на ньому з порозрядному операторами , такими як ~
, |
, &
, ^
, >>
і так далі, а не з арифметичними операціями , такими як +
, -
, *
, і /
т.д.
Беззнаковий тут ідеальний, оскільки поведінка побітових операторів чітко визначена та стандартизована. Підписані значення мають кілька проблем, таких як невизначена та невизначена поведінка під час переміщення та невизначене представлення.
Коли ви насправді хочете модульну арифметику. Іноді ви насправді хочете 2 ^ N модульної арифметики. У цих випадках "переповнення" - це функція, а не помилка. Беззнакові значення дають вам те, що ви хочете тут, оскільки вони визначені для використання модульної арифметики. Підписані значення взагалі не можна (легко, ефективно) використовувати, оскільки вони мають невизначене представлення, а переповнення невизначене.
0,5 Після того, як я написав це, я зрозумів, що це майже ідентично прикладу Джарода , якого я не бачив - і з поважних причин, це хороший приклад!
1 Ми говоримо size_t
тут, тому зазвичай 2 ^ 32-1 у 32-розрядної системі або 2 ^ 64-1 у 64-розрядної.
2 У C ++ це не зовсім так, оскільки непідписані значення містять більше значень у верхньому кінці, ніж відповідний підписаний тип, але основна проблема полягає в тому, що маніпулювання безпідписаними значеннями може призвести до (логічно) підписаних значень, але відповідної проблеми немає із підписаними значеннями (оскільки підписані значення вже включають непідписані значення).
unsigned int x = 0; --x;
і подивіться, щоx
станеться. Без обмежень перевірки розмір може раптово отримати якесь несподіване значення, яке може легко призвести до UB.