Чи використання непідписаного, а не підписаного int, швидше за все, спричиняє помилки? Чому?


82

У посібнику стилю Google C ++ на тему "Непідписані цілі числа" пропонується

Через історичну аварію стандарт С ++ також використовує цілі числа без підпису для представлення розміру контейнерів - багато членів органу, що відповідає стандартам, вважають, що це помилка, але на даний момент це фактично неможливо виправити. Той факт, що непідписана арифметика не моделює поведінку простого цілого числа, а натомість визначається стандартом для моделювання модульної арифметики (обертання на переповнення / недолив), означає, що компілятор не може діагностувати значний клас помилок.

Що не так з модульною арифметикою? Хіба це не очікувана поведінка непідписаного int?

До яких помилок (значного класу) посилається керівництво? Переповнені помилки?

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

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


4
Спробуйте зробити unsigned int x = 0; --x;і подивіться, що xстанеться. Без обмежень перевірки розмір може раптово отримати якесь несподіване значення, яке може легко призвести до UB.
Якийсь програміст, чувак,

33
Принаймні беззнакове переповнення має чітко визначену поведінку та дає очікувані результати.
user7860670

35
На не пов’язану (до вашого запитання, але не до керівництв стилів Google), якщо ви трохи пошукаєте, ви знайдете деяку (іноді справедливо) критику щодо керівництва стилями Google. Не сприймайте їх як євангелію.
Якийсь чувак-програміст

18
З іншого боку, intпереповнення та недолив - це UB. Ви рідше стикаєтесь із ситуацією, коли людина intнамагається висловити значення, якого вона не може, ніж ситуація, яка зменшується unsigned intнижче нуля, але люди, які будуть здивовані поведінкою unsigned intарифметики, - це ті люди, які можуть також написати код, який спричинить intпереповнення UB, як використання a < a + 1для перевірки переповнення.
Франсуа Андріє

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

Відповіді:


71

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

На моєму досвіді, поза змішаними порівняннями та правилами просування, є дві основні причини, чому беззнакові значення є магнітами помилок наступним чином.

Беззнакові значення мають розрив у нулі, найпоширеніше значення в програмуванні

Як цілі числа без знака, так і зі знаком мають 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++) { // do something }

Це чудово працює, поки одного разу ви не передасте порожній вектор. Замість того, щоб робити нульові ітерації, ви отримуєте v.size() - 1 == a giant number1, а ви зробите 4 мільярди ітерацій і майже матимете вразливість до переповнення буфера.

Вам потрібно написати це так:

for (size_t i = 0; i + 1 < v.size(); i++) { // do something }

Тож це можна "виправити" у цьому випадку, але лише ретельно продумавши непозначений характер 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 ++ це не зовсім так, оскільки непідписані значення містять більше значень у верхньому кінці, ніж відповідний підписаний тип, але основна проблема полягає в тому, що маніпулювання безпідписаними значеннями може призвести до (логічно) підписаних значень, але відповідної проблеми немає із підписаними значеннями (оскільки підписані значення вже включають непідписані значення).


10
Я погоджуюся з усім, що ви розмістили, але "64 біт має бути достатньо для всіх", звичайно здається занадто близьким до "640 тис. Має бути достатньо для всіх".
Andrew Henle

6
@Andrew - так, я ретельно підбирав слова :).
BeeOnRope

4
"64-біт закриває двері для знаків без підпису" -> Не погоджуюсь. Деякі цілочисельні завдання програмування є простими, не є випадком підрахунку і не потребують від'ємних значень, але потребують потужності-2 ширини: Паролі, шифрування, розрядна графіка, вигода з непідписаною математикою. Багато ідей тут вказати, чому код може використовувати підписану математику , коли в стані, але падає дуже короткий зробити без знака типу марного і закриваючи двері на них.
chux

2
@Deduplicator - так, я його залишив, оскільки це здається більш-менш схожим на краватку. На стороні непідписаного мода-2 ^ N обгортання ви, принаймні, маєте визначену поведінку, і ніякі несподівані "оптимізації" не влетять. На стороні UB будь-яке переповнення під час арифметики на непідписаному або підписаному, ймовірно, є помилкою в переважній більшості випадків ( -ftrapvкрім тих небагатьох, хто очікує арифметики модів), і компілятори надають такі варіанти, що можуть вловити всі підписані переповнення, але не всі беззнакові переповнення. Вплив на продуктивність не надто поганий, тому -ftrapvв деяких сценаріях може бути доцільно компілювати .
BeeOnRope

2
@BeeOnRope That's about the age of the universe measured in nanoseconds.Я сумніваюся в цьому. Всесвіт - це 13.7*10^9 yearsстаре, що є 4.32*10^17 sабо 4.32*10^26 ns. Щоб представляти 4.32*10^26як int, вам потрібно принаймні 90 bits. 9,223,372,036,854,775,807 nsбуло б лише про 292.5 years.
Осіріс

37

Як вже говорилося, змішування unsignedі signedможе привести до несподіваного поведінки (навіть якщо він добре визначено).

Припустимо, ви хочете переглядати всі елементи вектора, крім останніх п’яти, ви можете помилково написати:

for (int i = 0; i < v.size() - 5; ++i) { foo(v[i]); } // Incorrect
// for (int i = 0; i + 5 < v.size(); ++i) { foo(v[i]); } // Correct

Припустимо v.size() < 5, тоді, як v.size()є unsigned, s.size() - 5було б дуже велике число, і це i < v.size() - 5було б trueдля більш очікуваного діапазону значень i. І UB тоді відбувається швидко (один раз із обмеженого доступу i >= v.size())

Якби v.size()повертало підписане значення, тоді s.size() - 5було б від’ємним, а у наведеному вище випадку умова негайно було б помилковим.

З іншого боку, індекс повинен бути між [0; v.size()[тим, що unsignedмає сенс. Signed також має власну проблему як UB з переповненням або визначеною реалізацією поведінкою для зрушення вправо від’ємного підписаного числа, але рідше джерелом помилки для ітерації.


2
Хоча я сам використовую підписані номери, коли тільки можу, не думаю, що цей приклад є досить сильним. Той, хто тривалий час використовує беззнакові числа, напевно знає цю ідіому: замість цього i<size()-Xслід писати i+X<size(). Звичайно, це річ пам’ятати, але, на мій погляд, не так важко звикнути.
geza

8
Ви говорите, що в основному потрібно знати мову та правила примусу між типами. Я не бачу, як це змінюється залежно від того, чи використовується підписаний чи безпідписаний запитання. Не те, що я взагалі рекомендую використовувати підписаний, якщо немає потреби в негативних значеннях. Я погоджуюсь з @geza, використовуйте підписані лише за необхідності. Це робить керівництво Google сумнівною в кращому випадку . Імо це погана порада.
занадто чесний для цього сайту

2
@toohonestforthissite Справа в тому, що правила є таємними, безшумними та основними причинами помилок. Використання виключно підписаних типів для арифметики позбавляє вас від проблеми. До речі, використання непідписаних типів з метою дотримання позитивних цінностей є одним з найгірших зловживань для них.
Перехожий

2
На щастя, сучасні компілятори та середовища розробки ID дають попередження при змішуванні підписаних та беззнакових чисел у виразі.
Олексій Б.

5
@PasserBy: Якщо ви називаєте їх таємничими, вам доведеться додати цілочисельні акції та UB для переповнення підписаних типів arcane. І найпоширеніший оператор sizeof повертає непідписаний у будь-якому випадку, тому вам доведеться знати про них. Сказав, що: якщо ви не хочете вивчати деталі мови, просто не використовуйте C або C ++! Враховуючи, що Google просуває go, можливо, це саме їх мета. Часи "не будь злим" давно минули ...
занадто чесно для цього сайту

20

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

#include <iostream>
int main()  {
    auto qualifier = -1 < 1u ? "makes" : "does not make";
    std::cout << "The world " << qualifier << " sense" << std::endl;
}

Вихід:

Світ не має сенсу

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

Моделювання типу без підпису на основі очікуваного домену значень ваших чисел - погана ідея. Більшість цифр ближче до 0, ніж до 2 мільярдів, тому для типів без знака багато ваших значень знаходяться ближче до краю дійсного діапазону. Що ще гірше, кінцеве значення може знаходитись у відомому позитивному діапазоні, але під час обчислення виразів проміжні значення можуть перевищувати рівень, і якщо вони використовуються в проміжній формі, можуть бути ДУЖЕ неправильні значення. Нарешті, навіть якщо очікується, що ваші значення завжди будуть позитивними, це не означає, що вони не будуть взаємодіяти з іншими змінними, які можуть бути від’ємними, і тому ви опинитеся у вимушеній ситуації змішування типів зі знаком та без знака, що є найгірше місце бути.


8
Моделювання типу без підпису на основі очікуваного домену значень ваших чисел є поганою ідеєю *, якщо ви не ставитесь до неявних перетворень як до попереджень і лінуєтесь використовувати належний тип приводок. * Моделювання типів на їх очікуваному дійсному Значення цілком обґрунтоване, тільки не в C / C ++ із вбудованими типами.
villasv

1
@ user7586189 Це хороша практика, коли неможливо створити недійсні дані, тому наявність змінних, що мають лише позитивні значення, є цілком розумним. Але ви не можете тонко налаштувати вбудовані типи C / C ++, щоб заборонити за замовчуванням погані склади, подібні до наведеної у цій відповіді, і дійсність в кінцевому підсумку є відповідальністю когось іншого. Якщо ви розмовляєте мовою з більш суворим складом (навіть між вбудованими), моделювання очікуваного домену є досить хорошою ідеєю.
villasv

1
Зверніть увагу, я навіть згадувати провертаючи попередження і встановлювати їх на помилки, але не кожен робить. Я все ще не погоджуюсь з @villasv з вашим твердженням про моделювання цінностей. Вибираючи непідписаний, ви ТАКОЖ неявно моделюєте кожну іншу цінність, з якою вона може зіткнутися, не надто передбачаючи, що це буде. І майже напевно помиляється.
Chris Uzdavinis

1
Моделювання з урахуванням домену - це добре. Використання unsigned для моделювання домену НЕ. (Підписаний проти непідписаного слід вибирати на основі типів використання , а не діапазону значень , якщо тільки неможливо зробити інакше.)
Кріс Уздавініс,

2
Як тільки у вашій кодовій базі є поєднання знакових підписів і без знаків, коли ви виявляєте попередження та просуваєте їх до помилок, код закінчується всією static_casts, щоб зробити перетворення явними (оскільки математика все одно повинна бути зроблена.) Навіть коли правильно, він схильний до помилок, важче працювати з ним і важче читати.
Кріс Уздавініс

11

Чому використання непідписаного int частіше спричиняє помилки, ніж використання підписаного int?

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

Використовуйте відповідний інструмент для роботи.

Що не так з модульною арифметикою? Хіба це не очікувана поведінка непідписаного int?
Чому використання непідписаного int частіше спричиняє помилки, ніж використання підписаного int?

Якщо завдання добре підібране: нічого поганого. Ні, не вірогідніше.

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

Алгоритми стиснення / декомпресії також, як і різні графічні формати, виграють і менш глючать із непідписаною математикою.

Будь-який час, коли використовуються побітні оператори та зсуви, непідписані операції не плутаються з проблемами розширення знаків із підписаною математикою.


Підписана цілочисельна математика має інтуїтивний вигляд і відчуває, що її легко зрозуміти всі, включаючи тих, хто навчається кодуванню. C / C ++ не був націлений спочатку, і зараз він не повинен бути мовою вступу. Для швидкого кодування, в якому використовуються захисні мережі щодо переповнення, інші мови краще підходять. Щодо пісного швидкого коду, С припускає, що кодери знають, що вони роблять (вони мають досвід).

Ловушкою підписаної математики сьогодні є всюдисущий 32-розрядний, intякий із такою кількістю проблем досить широкий для загальних завдань без перевірки діапазону. Це призводить до самозаспокоєння, що переповнення не кодується. Натомість for (int i=0; i < n; i++) int len = strlen(s);це розглядається як ОК, оскільки nпередбачається, що < INT_MAXі рядки ніколи не будуть занадто довгими, а не захищені в повному діапазоні в першому випадку або з використаннямsize_t , unsignedабо навіть long longу 2 - й.

C / C ++, розроблений в епоху, яка включала 16-бітну, а також 32-бітну, intа додатковий біт - 16-бітову без підписуsize_t був значним. Потрібна була увага щодо питань переповнення, будь то intчи unsigned.

З 32-розрядними (або ширшими) додатками Google на не 16-розрядних int/unsignedплатформах, передбачається відсутність уваги до +/- переповнення з intогляду на його широкий діапазон. Це має сенс заохочувати такі програмиint більш unsigned. Проте intматематика недостатньо захищена.

Вузький 16-розрядний int/unsigned проблеми стосуються деяких вбудованих програм.

Правила Google добре застосовуються до коду, який вони пишуть сьогодні. Це не остаточне керівництво для широкого діапазону коду C / C ++.


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

У C / C ++ переповнення підписаного int math є невизначеною поведінкою, і тому, звичайно, не простіше виявити, ніж визначену поведінку непідписаної математики.


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


2
Ви добре переконаєтесь, що an intтакож не моделює поведінку "фактичного" цілого числа. Невизначена поведінка при переповненні - це не те, як математик думає про цілі числа: у них немає можливості "переповнення" абстрактним цілим числом. Але це одиниці машинного зберігання, а не цифри математика.
christ

1
@tchrist: Беззнакова поведінка при переповненні - це те, як математик думав би про абстрактне алгебраїчне кільце цілочисельних конгруентних моди (type_MAX + 1).
supercat

Якщо ви використовуєте gcc, signed intпереповнення легко виявити (за допомогою -ftrapv), тоді як беззнакове "переповнення" важко виявити.
anatolyg

5

Я маю певний досвід роботи з керівництвом по стилю Google, а також Посібником автостопа до божевільних вказівок від поганих програмістів, котрі входили в компанію вже давно. Ця конкретна настанова - лише один із прикладів десятків горіхових правил у цій книзі.

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

Ідея використання арифметичних типів (наприклад, підписаних цілих чисел) для зберігання розмірів контейнерів є ідіотською. Чи використовували б ви дубль для зберігання розміру списку теж? Те, що в Google люди зберігають розміри контейнерів з використанням арифметичних типів і вимагають від інших робити те саме, говорить щось про компанію. Одне, що я помічаю в таких диктатах, це те, що чим вони дурніші, тим більше їм потрібно дотримуватися суворих правил «роби це, або тебе звільняють», бо інакше люди зі здоровим глуздом ігноруватимуть це правило.


Поки я отримую ваш дрейф, зроблені загальні висловлювання практично усувають побітові операції, якщо unsignedтипи можуть містити лише відліки і не використовуватись в арифметиці. Тож частина "Божевільні директиви поганих програмістів" має більше сенсу.
Девід К. Ранкін,

@ DavidC.Rankin Будь ласка, не сприймайте це як "загальну" заяву. Очевидно, що існує безліч законних застосувань для цілих чисел без знака (наприклад, зберігання побітових значень).
Тайлер Дурден,

Так, так - я цього не зробив, тому я сказав: "Я розумію тебе".
Девід К. Ранкін,

1
Підрахунки часто порівнюють з речами, на яких зроблена арифметика, наприклад, з індексами. Те, як C обробляє порівняння, що включають підписані та беззнакові числа, може призвести до багатьох дивних химерностей. За винятком ситуацій, коли верхнє значення підрахунку містилося б у непідписаному, але не відповідному підписаному типі (поширене в дні, коли intбуло 16 біт, але набагато менше сьогодні), краще мати підрахунки, які поводяться як числа.
supercat

1
"Помилки трапляються лише з непідписаними типами, якщо ви намагаєтесь робити з ними арифметику" - що відбувається постійно. "Ідея використання арифметичних типів (наприклад, підписаних цілих чисел) для зберігання розмірів контейнерів є ідіотською" - це не так, і комітет С ++ зараз вважає історичною помилкою використання size_t. Причина? Неявні перетворення.
Атіла Невес,

1

Використання беззнакових типів для представлення невід’ємних значень ...

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

Правила кодування Google акцентують увагу на першому вигляді розгляду. Інші набори орієнтирів, такі як основні настанови C ++ , більше підкреслюють другий пункт. Наприклад, розглянемо Основні вказівки I.12 :

I.12: Оголосіть покажчик, який не повинен мати значення null як not_null

Причина

Щоб уникнути помилок відсилання посилань на nullptr. Для підвищення продуктивності, уникаючи зайвих перевірок на nullptr.

Приклад

int length(const char* p);            // it is not clear whether length(nullptr) is valid
length(nullptr);                      // OK?
int length(not_null<const char*> p);  // better: we can assume that p cannot be nullptr
int length(const char* p);            // we must assume that p can be nullptr

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

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


0

Заява google стосується використання unsigned як типу розміру для контейнерів . На відміну від цього, питання видається більш загальним. Будь ласка, майте це на увазі, читаючи далі.

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

Підписані розміри контейнера

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

Зараз існує клас людей, які люблять говорити про математику та те, що в цьому контексті є "природним". Як інтегральний тип з від’ємним числом може бути природним для опису чогось, що за своєю суттю> = 0? Багато використовуєте масиви з від’ємними розмірами? ІМХО, особливо люди, схильні до математичної діяльності, вважатимуть це невідповідність семантики (розмір / тип індексу каже, що можливий негативний, тоді як масив негативного розміру важко уявити) дратує.

Отже, єдине питання, що залишається з цього питання, полягає в тому, якщо - як зазначено в коментарі google - компілятор може насправді активно допомагати у пошуку таких помилок. І навіть краще, ніж альтернатива, яка буде захищена від недозволених цілих беззнакових чисел (збірка x86-64 та, можливо, інші архітектури мають засоби для досягнення цього, лише C / C ++ не використовує ці засоби). Єдиний спосіб, який я можу зрозуміти, це якщо компілятор автоматично додає перевірку часу виконання ( if (index < 0) throwOrWhatever) або у разі дії часу компіляції видає багато потенційно хибнопозитивних попереджень / помилок "Індекс для цього доступу до масиву може бути негативним." Я сумніваюся, це було б корисно.

Крім того, люди, які насправді пишуть перевірки виконання для своїх індексів масивів / контейнерів, це більше робота, пов’язана із підписаними цілими числами. Замість того, щоб писати, if (index < container.size()) { ... }вам тепер потрібно написати:if (index >= 0 && index < container.size()) { ... } . Для мене це схоже на примусову працю, а не на покращення ...

Мови без типів без підпису смоктати ...

Так, це удар в Java. Зараз я походжу із вбудованого програмного забезпечення, і ми багато працювали з польовими шинами, де двійкові операції (і, або, xor, ...) та трохи мудрий склад значень - це буквально хліб та масло. Для одного з наших продуктів ми - а точніше замовник - хотіли порту Java ... і я сів навпроти дуже компетентного хлопця, який здійснив порт (я відмовився ...). Він намагався зберігати спокій ... і страждати мовчки ... але біль відчувався, він не міг припинити лайку після кількох днів постійної роботи з підписаними інтегральними значеннями, які ПОВИННІ бути непідписаними ... Навіть написання модульних тестів для ці сценарії болючі, і мені, особисто, я думаю, що Java було б краще, якби вони пропустили підписані цілі числа і просто запропонували без підпису ... принаймні тоді, вам не потрібно дбати про розширення знаків тощо ...

Це мої 5 центів з цього питання.

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