Використання непідписаних цілих чисел у C та C ++


23

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

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

У той же час різні посібники з кодування, які я читаю (наприклад, Google), перешкоджають використанню непідписаних цілочисельних типів, і наскільки я не знаю, ні Java, ні Scala не мають цільових цілей без підпису.

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


Відповіді:


31

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

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

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


8
Компілятор виявляє змішані арифметичні підписи без підпису; просто тримайте попередження без збірки (з достатньо високим рівнем попередження). Крім того, intкоротше набирати :)
rucamzu

7
Сповідь: Я з другою школою думки, і хоча я розумію міркування щодо неподписаних типів: intце більш ніж достатньо для індексів масиву в 99,99% разів. Питання арифметики без підпису набагато частіше зустрічаються, і тому вони мають перевагу в плані того, чого слід уникати. Так, компілятори попереджають вас про це, але скільки попереджень ви отримуєте під час компіляції будь-якого масштабного проекту? Ігнорувати попередження небезпечно, і погана практика, але в реальному світі ...
Еліас Ван Оотегем

11
+1 до відповіді. Застереження : Тупі думки попереду : 1: Моя відповідь на другу школу думки полягає в тому, що: я ставлю гроші, що кожен, хто отримає несподівані результати з непідписаних цілісних типів на С, буде мати невизначене поведінку (а не суто академічну) їх нетривіальні програми С, які використовують підписані цілісні типи. Якщо ви не знаєте достатньо C, щоб вважати, що неподписані типи є кращими для використання, я раджу уникати C. 2: В індексах та розмірах масивів у C є точно один правильний тип, і це size_t, якщо немає спеціального випадку хороша причина в іншому випадку.
mtraceur

5
Ви натрапляєте на проблеми без змішаного підпису. Просто обчисліть неподписаний int мінус unsigned int.
gnasher729

4
Не сприймаючи з вами проблеми, Саймон, лише з першою школою думки, яка стверджує, що "є деякі поняття, які за своєю суттю не підписані - такі, як індекси масиву". конкретно: "Є точно один правильний тип для індексів масиву ... в C", фігня! . Ми, DSPers, весь час використовуємо негативні показники. особливо з парними або непарними симетричними імпульсними реакціями, які є безпричинними. і для математики LUT Я перебуваю у другій школі думки, але думаю, що корисно мати як підписані, так і неподписані цілі числа в C і C ++.
Роберт Брістоу-Джосон

21

Перш за все, інструкція з кодування Google C ++ не є дуже хорошою, яку слід дотримуватися: вона уникає таких речей, як винятки, прискорення тощо, які є елементами сучасного C ++. По-друге, те, що певна інструкція працює для компанії X, не означає, що вона буде для вас правильною. Я б продовжував використовувати неподписані типи, оскільки у вас є гарна потреба в них.

Достойне правило для C ++: віддавайте перевагу, intякщо у вас немає вагомих причин використовувати щось інше.


8
Це зовсім не те, що я маю на увазі. Конструктори призначені для встановлення інваріантів, і оскільки вони не є функціями, вони не можуть просто, return falseякщо цей інваріант не встановлений. Таким чином, ви можете або розділити речі, і використовувати функції init для своїх об'єктів, або ви можете кинути std::runtime_error, нехай відбувається розмотування стека, і дозвольте всі ваші об’єкти RAII самостійно очистити, і ви, розробник, зможете обробляти виняток там, де це зручно ти це зробиш.
bstamour

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

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

6
@ zzz777 Не використовуйте винятки? Чи мають приватні конструктори, які обгортають загальнодоступні фабричні функції, які фіксують винятки і роблять що - повертають a nullptr? повернути об’єкт "за замовчуванням" (що б це не означало)? Ви нічого не вирішили - ви просто сховали проблему під килимом, і сподіваєтесь, ніхто цього не дізнається.
Mael

5
@ zzz777 Якщо ви все-таки збираєтеся розбити поле, чому ви дбаєте, якщо це відбувається за винятком або signal(6)? Якщо ви використовуєте виняток, 50% розробників, які знають, як з ними боротися, можуть написати хороший код, а решта можуть перевезти їх однолітки.
IllusiveBrian

6

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

Розглянемо використання стандартного size_t як індексу масиву:

for (size_t i = 0; i < n; ++i)
    // do something here;

Гаразд, абсолютно нормально. Тоді, подумайте, ми вирішили чомусь змінити напрям петлі:

for (size_t i = n - 1; i >= 0; --i)
    // do something here;

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

Ще одна причина для мене - набридливі попередження, які змушують кожного разу писати щось подібне :

int n = 123;  // for some reason n is signed
...
for (size_t i = 0; i < size_t(n); ++i)

Це незначні речі, але вони складаються. Я відчуваю, що код чистіший, якщо всюди використовуються лише підписані цілі числа.

Редагувати: Звичайно, приклади виглядають тупо, але я бачив, як люди роблять цю помилку. Якщо існує такий простий спосіб уникнути цього, чому б не використати його?

Коли я компілюю наступний фрагмент коду з VS2015 або GCC, я не бачу попереджень із налаштуваннями попередження за замовчуванням (навіть із -Wall для GCC). Вам потрібно попросити -Wextra, щоб отримати попередження про це в GCC. Це одна з причин, коли ви завжди повинні компілювати з Wall і Wextra (і використовувати статичний аналізатор), але у багатьох проектах реального життя люди цього не роблять.

#include <vector>
#include <iostream>


void unsignedTest()
{
    std::vector<int> v{ 1, 2 };

    for (int i = v.size() - 1; i >= 0; --i)
        std::cout << v[i] << std::endl;

    for (size_t i = v.size() - 1; i >= 0; --i)
        std::cout << v[i] << std::endl;
}

int main()
{
    unsignedTest();
    return 0;
}

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

1
У минулому я вдавався до таких жахів, for (size_t i = n - 1; i < n; --i)щоб змусити це працювати правильно.
Саймон Б

2
Говорячи про size_tfor (size_t revind = 0u; revind < n; ++revind) { size_t ind = n - 1u - revind; func(ind); }
фор

2
@rwong Omg, це некрасиво. Чому б просто не використовувати int? :)
Олексій Петренко

1
@AlexeyPetrenko - зауважте, що ні діючі стандарти C, ні C ++ не гарантують, що intє достатньо великим, щоб вмістити всі дійсні значення size_t. Зокрема, вони intможуть дозволяти числа лише до 2 ^ 15-1, і зазвичай це робиться в системах, які мають обмеження розподілу пам’яті 2 ^ 16 (або в деяких випадках навіть вище). longможе бути більш безпечним, хоча все ще не гарантовано . Тільки size_tгарантовано працює на всіх платформах і в усіх випадках.
Жуль

4
for (size_t i = v.size() - 1; i >= 0; --i)
   std::cout << v[i] << std::endl;

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

for (size_t i = v.size(); i-- > 0; )
    std::cout << v[i] << std::endl;

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

size_t i = v.size();
while (i > 0)
{
    --i;
    std::cout << v[i] << std::endl;
}

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

Мені б пощастило, якби курси для початківців не лише показували правильний цикл для підписаних, але й для неподписаних типів. Це дозволить уникнути пари помилок, за які IMHO повинен звинувачувати мимоволі розробників, а не звинувачувати неподписаний тип.

HTH


1

Непідписані цілі числа є з причини.

Розглянемо, наприклад, передачу даних у вигляді окремих байтів, наприклад, у мережевому пакеті або файловому буфері. Ви можете час від часу стикатися з такими звірами, як 24-бітні цілі числа. Легко переміщений біт з трьох 8-бітових непідписаних цілих чисел, не так просто з 8-бітовими цілими числами, підписаними.

Або подумайте про алгоритми, використовуючи таблиці пошуку символів. Якщо символ є 8-бітним цілим числом без підпису, ви можете проіндексувати таблицю пошуку за значенням символу. Однак що робити, якщо мова програмування не підтримує непідписані цілі числа? У вас буде негативний індекс до масиву. Ну, я думаю, ви могли б використовувати щось на кшталт, charval + 128але це просто потворно.

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

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

Іноді ефективність має значення настільки, що вам справді потрібен цей додатковий біт непідписаних цілих чисел. Розглянемо для прикладу пристрої IoT, які постачаються мільйонами. Після цього багато ресурсів програмування може бути виправдано витрачено на мікрооптимізацію.

Я можу стверджувати, що обґрунтування уникнення використання непідписаних цілих типів (змішана арифметика знаків, змішане зіставлення знаків) може бути подолане компілятором з належними попередженнями. Такі застереження зазвичай не вмикаються за замовчуванням, але дивіться, наприклад, -Wextraабо окремо -Wsign-compare(автоматично ввімкнено в C за допомогою -Wextra, хоча я не думаю, що це автоматично ввімкнено в C ++) та -Wsign-conversion.

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


0

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

Є багато випадків, коли ви робите арифметику з цілими числами. У цих випадках використовуйте підписані цілі числа, щоб уникнути неправильної поведінки навколо нуля. Перегляньте велику кількість прикладів із циклами, коли цикл до нуля використовує дуже неінтуїтивний код або порушується через використання непідписаних чисел. Існує аргумент "але індекси ніколи не є негативними" - точно, але, наприклад, відмінності індексів негативні.

У дуже рідкісному випадку, коли індекси перевищують 2 ^ 31, але не 2 ^ 32, ви не використовуєте непідписані цілі числа, ви використовуєте 64 бітні цілі числа.

Нарешті, хороша пастка: У циклі "for (i = 0; i <n; ++ i) a [i] ...", якщо я не підписаний 32-бітовим, а пам'ять перевищує 32-бітові адреси, компілятор не може оптимізувати доступ до [i] шляхом збільшення покажчика, тому що при i = 2 ^ 32 - 1 я обертається навколо. Навіть коли п ніколи не стає таким великим. Використання підписаних цілих чисел уникає цього.


-5

Нарешті, я знайшов тут справді хорошу відповідь: «Кулінарна книга безпечного програмування» Дж. Вігі та М. Мессьє ( http://shop.oreilly.com/product/9780596003944.do )

Проблеми безпеки з підписаними цілими числами:

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

Існують проблеми з підписаними <-> непідписаними перетвореннями, тому не доцільно використовувати mix.


1
Чому це хороша відповідь? Що таке рецепт 3.5? Що це говорить про цілочисельне переповнення тощо?
Балдрікк

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

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

FYI щойно дізнався про ще одну причину використання непідписаних цілих чисел: можна легко виявити переповнення: youtube.com/…
zzz777
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.