Чому для вбудованого коду слід використовувати типи "uint_t" замість "непідписаний int"?


22

Я пишу заявку в c для STM32F105, використовуючи gcc.

У минулому (з більш простими проектами), я завжди визначаються змінні , як char, int, unsigned intі так далі.

Я бачу , що він є загальним для використання типи , певні в stdint.h, такі як int8_t, uint8_t, uint32_tі т.д. Це правда , що в багатокорпусних API, який я використовую, а також в бібліотеці ARM CMSIS від ST.

Я вважаю, що я розумію, чому ми повинні так робити; щоб дозволити компілятору краще оптимізувати простір пам'яті. Я думаю, що можуть бути додаткові причини.

Однак, через цілі правила просування c, я продовжую працювати з попередженнями про перетворення будь-коли, коли я намагаюся додати два значення, виконувати побітну операцію тощо. Попередження читає щось подібне conversion to 'uint16_t' from 'int' may alter its value [-Wconversion]. Питання обговорюється тут і тут .

Це не відбувається при використанні змінних, оголошених як intабо unsigned int.

Наведіть пару прикладів, враховуючи це:

uint16_t value16;
uint8_t value8;

Я повинен був би змінити це:

value16 <<= 8;
value8 += 2;

до цього:

value16 = (uint16_t)(value16 << 8);
value8 = (uint8_t)(value8 + 2);

Це некрасиво, але я можу це зробити за потреби. Ось мої запитання:

  1. Чи є випадок, коли перехід від безпідписаного до підписаного та назад до непідписаного зробить результат неправильним?

  2. Чи є інші великі причини для / проти використання цілочисельних типів stdint.h?

Виходячи з отриманих відповідей, схоже, типи stdint.h, як правило, бажані, навіть якщо c перетворюється uintна intта назад. Це призводить до більшого питання:

  1. Я можу запобігти попередженням компілятора, використовуючи typecasting (наприклад, value16 = (uint16_t)(value16 << 8);). Чи я просто приховую проблему? Чи є кращий спосіб зробити це?

Використовуйте непідписані літерали: тобто 8uі 2u.
Зупиніть шкодити Моніці

Дякую, @OrangeDog, я думаю, що я нерозумію. Я спробував і те, value8 += 2u;і value8 = value8 + 2u;, але отримую однакові попередження.
бітмейк

Використовуйте їх у будь-якому випадку, щоб уникнути підписаних попереджень, коли у вас ще немає попереджень про ширину :)
Зупиніть шкодити Моніці

Відповіді:


11

Компілятор, що відповідає стандартам, де intбув від 17 до 32 біт, може законно робити все, що завгодно, із наступним кодом:

uint16_t x = 46341;
uint32_t y = x*x; // temp result is signed int, which can't hold 2147488281

Реалізація, яка хотіла зробити це, могла б законно генерувати програму, яка б нічого не робила, крім виведення рядка "Fred" повторно на кожен контактний порт, використовуючи кожен уявний протокол. Імовірність того, що програма перенесеться на реалізацію, яка б зробила таке, є вкрай низькою, але теоретично це можливо. Якщо ви хочете написати вищевказаний код, щоб гарантовано не брати участь у невизначеному поведінці, то слід написати останній вираз як (uint32_t)x*xабо 1u*x*x. У компіляторі, де intзнаходиться між 17 і 31 бітом, останній вираз буде вимикати верхні біти, але не буде залучатися до не визначеної поведінки.

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

Зауважте, що використання таких типів, як intі shortможе усунути деякі попередження та виправити деякі проблеми, але, ймовірно, створить інші. Взаємодія між типами типу uint16_tі правилами цілого просування C є хиткою, але такі типи все ще є кращими, ніж будь-яка альтернатива.


6

1) Якщо ви просто переходите від безпідписаного до підписаного цілого числа однакової довжини вперед і назад, без жодних операцій між ними, ви будете отримувати однаковий результат кожного разу, тому тут немає жодних проблем. Але різні логічні та арифметичні операції діють по-різному на підписані та непідписані операнди.
2) Основна причина використання stdint.hтипів є те , що розмір битого таких типів А визначені і так само у всіх платформах, які не вірно для int, і longт.д., а також charне має ніякого стандарту signess, він може бути знаком або без знаку шляху за замовчуванням. Це полегшує маніпулювання даними, знаючи точний розмір, не використовуючи додаткові перевірки та припущення.


2
Розміри int32_tта uint32_tрівні на всій платформі, на якій вони визначені . Якщо процесор не має точно відповідного типу апаратного забезпечення, ці типи не визначаються. Звідси перевага intтощо і, можливо, int_least32_tтощо
Піт Бекер

1
@ PeteBecker - Це, мабуть, перевага, тому що отримані помилки компіляції дають вам негайно знати про проблему. Я набагато скоріше, ніж мої типи, що змінюють розмір на мені.
сапі

@sapi - у багатьох ситуаціях базовий розмір не має значення; Програмісти на C багато років добре ладнали без фіксованих розмірів.
Піт Бекер

6

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

MISRA (directive 4.6): "typedefs that indicate size and signedness should be used in place of the basic types".

Також Джек Гансл, схоже, є прихильником цього правила: http://www.ganssle.com/tem/tem265.html


2
Дуже погано, що немає жодних типів для визначення "N-бітового цілого числа без підпису, яке може бути безпечно помножено на будь-яке інше ціле число одного розміру, щоб отримати результат однакового розміру". Правила просування цілочисень жахливо взаємодіють із наявними типами uint32_t.
supercat

3

Найпростіший спосіб усунути попередження - уникнути використання -Wconversion в GCC. Я думаю, що ви повинні ввімкнути цю опцію вручну, але якщо ні, ви можете використовувати -Wno-конверсію для її відключення. Ви можете ввімкнути попередження для перетворення точності знаків і FP за допомогою інших опцій , якщо ви все ще хочете їх.

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

-std = c99 -педантичний -Wall -Wextra -Wdowdow

Додайте більше, якщо вони вам знадобляться, але шансів ви не будете.

Якщо вам потрібно зберегти -Wconversion, ви можете трохи скоротити код, набравши цифровий операнд:

value16 <<= (uint16_t)8;
value8 += (uint8_t)2;

Однак це не просто читати без виділення синтаксису.


2

в будь-якому проекті програмного забезпечення дуже важливо використовувати визначення портативних типів. (навіть наступна версія того ж компілятора потребує цього розгляду.) Хороший приклад, я кілька років тому працював над проектом, де поточний компілятор визначав "int" як 8 біт. Наступна версія компілятора визначала "int" як 16 біт. Оскільки ми не використовували жодних переносних визначень для 'int', оперативної пам’яті (ефективно) вдвічі збільшився розмір, і багато послідовностей коду, які залежали від 8-бітового int, не вдалося. Використання визначення портативного типу дозволило б уникнути цієї проблеми (сотні людей, що годин на виправлення).


Жоден розумний код не повинен використовувати intдля позначення 8-бітного типу. Навіть якщо компілятор, що не стосується C, як CCS, робить це розумним кодом, слід використовувати charтип 8 або typedefed для 8 біт, а тип - тип decdefed (не "довгий") для 16 біт. З іншого боку, перенесення коду з чогось типу CCS до реального компілятора може бути проблематичним, навіть якщо він використовує належні typedefs, оскільки такі компілятори часто "незвичні" іншими способами.
supercat

1
  1. Так. Ціле число, підписане n біт, може представляти приблизно половину кількості невід’ємних чисел як n-бітне ціле число, яке не підписується, і покладання на характеристики переповнення є невизначеним поведінкою, тому все може статися. Переважна більшість поточних та минулих процесорів використовують доповнення двійок, тому багато операцій трапляються однаково на підписаних та непідписаних інтегральних типах, але навіть тоді не всі операції дають битові ідентичні результати. Ви дійсно просите додаткових проблем пізніше, коли не можете зрозуміти, чому ваш код не працює за призначенням.

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

У тих випадках, коли мені дійсно потрібен кращий контроль над розміром чи підписаністю типу, я зазвичай віддаю перевагу або використовувати визначений системою typedef (size_t, intmax_t тощо), або робити власний typedef, який вказує на функцію заданої тип (prng_int, adc_int тощо).


0

Часто код використовується на великому пальці ARM та AVR (і x86, powerPC та інших архітектурах), а 16 або 32 біт може бути ефективнішим (обидва способи: спалах і цикли) на STM32 ARM навіть для змінної, яка відповідає 8 бітам ( 8 біт ефективніше на AVR) . Однак якщо SRAM майже заповнений, повернення до 8 біт для глобальних варіантів може бути розумним (але не для місцевих варіантів). Для портативності та обслуговування (особливо для 8-бітових варіантів), вона має переваги (без будь-яких недоліків), щоб вказати МІНІМАЛЬНИЙ відповідний розмір , а не точний розмір та typedef в одному .h місці (як правило, під ifdef) для налаштування (можливо uint_fast8_t / uint_least8_t) під час перенесення / складання, наприклад:

// apparently uint16_t is just as efficient as 32 bit on STM32, but 8 bit is punished (with more flash and cycles)
typedef uint16_t uintG8_t; // 8bit if SRAM is scarce (use fol global vars that fit in 8 bit)
typedef uint16_t uintL8_t; // 8bit on AVR (local var, 16 or 32 bit is more efficient on STM + less flash)
// might better reserve 32 bits on some arch, STM32 seems efficient with 16 bits:
typedef uint16_t uintG16_t; // 16bit if SRAM is scarce (use fol global vars that fit in 16 bit)
typedef uint16_t uintL16_t; // 16bit on AVR (local var, 16 or 32 bit whichever is more efficient on other arch)

Бібліотека GNU трохи допомагає, але як правило, typedefs має сенс:

typedef uint_least8_t uintG8_t;
typedef uint_fast8_t uintL8_t;

// але uint_fast8_t для BOTH, коли SRAM не викликає проблем.

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