Для встроенного кода, почему я должен использовать типы «uint_t» вместо «unsigned 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. Я могу предотвратить предупреждения компилятора, используя приведение типов (например value16 = (uint16_t)(value16 << 8);). Я просто скрываю проблему? Есть ли лучший способ сделать это?
bitsmack
источник
Используйте литералы без знака: т.е. 8uи 2u.
Стоп Harm Monica
Спасибо, @OrangeDog, я думаю, что я неправильно понимаю. Я пробовал и то value8 += 2u;и другое value8 = value8 + 2u;, но получаю одинаковые предупреждения.
bitmack
В любом случае, используйте их, чтобы избежать подписанных предупреждений, когда у вас еще нет предупреждений о ширине :)
Прекратите вредить Монике

Ответы:

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_tC и правилами целочисленного продвижения, является странным, но такие типы все еще, вероятно, лучше, чем любая другая альтернатива.

Supercat
источник
6

1) Если вы просто переводите целое число без знака в целое число со знаком одинаковой длины, без каких-либо промежуточных операций, вы будете получать один и тот же результат каждый раз, так что здесь нет проблем. Но различные логические и арифметические операции по-разному действуют на операнды со знаком и без знака.
2) Основная причина использования stdint.hтипов заключается в том, что размер битов таких типов определен и одинаков для всех платформ, что не соответствует действительности intи longт. Д., А также charне имеет стандартного значения, он может быть подписан или не подписан дефолт. Это облегчает манипулирование данными, зная точный размер, без дополнительных проверок и допущений.

Евгений Ш.
источник
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.
Суперкат
3

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

Предупреждения -Wconversion почти всегда являются ложными срабатываниями, поэтому, возможно, даже -Wextra не включает их по умолчанию. У вопроса переполнения стека есть много предложений для хороших наборов опций. Исходя из моего собственного опыта, это хорошее место для начала:

-std = c99 -pedantic -Wall -Wextra -Wshadow

Добавьте больше, если они вам нужны, но, скорее всего, вы этого не сделаете.

Если вам нужно сохранить -Wconversion, вы можете немного сократить свой код, просто введя числовой операнд:

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

Однако это не так легко прочитать без подсветки синтаксиса.

Адам Хаун
источник
2

В любом программном проекте очень важно использовать определения переносимых типов. (даже следующая версия того же компилятора нуждается в этом рассмотрении.) Хороший пример: несколько лет назад я работал над проектом, в котором текущий компилятор определил 'int' как 8 бит. Следующая версия компилятора определила 'int' как 16 бит. Поскольку мы не использовали переносимых определений для «int», ram (фактически) удвоился в размере, и многие кодовые последовательности, которые зависели от 8-битного int, потерпели неудачу. Использование определения переносимого типа позволило бы избежать этой проблемы (сотни человеко-часов для исправления).

Ричард Уильямс
источник
Никакой разумный код не должен использоваться intдля ссылки на 8-битный тип. Даже если компилятор без C, такой как CCS, делает это, разумный код должен использовать либо charтип с определением типа для 8 битов, либо тип с определением типа (не "long") для 16 битов. С другой стороны, перенос кода из чего-то вроде CCS в реальный компилятор может быть проблематичным, даже если он использует правильные определения типов, поскольку такие компиляторы часто «необычны» в других отношениях.
суперкат
1
  1. Да. N-разрядное целое число со знаком может представлять примерно половину числа неотрицательных чисел как n-разрядное целое число без знака, и полагаться на характеристики переполнения - неопределенное поведение, поэтому может произойти все что угодно. Подавляющее большинство современных и прошлых процессоров используют два дополнения, поэтому многие операции выполняют одно и то же для целочисленных типов со знаком и без знака, но даже тогда не все операции будут давать побитовые идентичные результаты. Позже вы действительно напрашиваетесь на дополнительные проблемы, когда не можете понять, почему ваш код работает не так, как задумано.

  2. Хотя int и unsigned имеют размеры, определенные реализацией, они часто выбираются «разумно» реализацией либо из соображений размера, либо из соображений скорости. Я обычно придерживаюсь этого, если у меня нет веских причин поступать иначе. Аналогично, при рассмотрении вопроса о том, использовать ли int или unsigned, я обычно предпочитаю int, если только у меня нет веских причин сделать это иначе.

В тех случаях, когда мне действительно нужно лучше контролировать размер или подпись типа, я обычно предпочитаю либо использовать системный typedef (size_t, intmax_t и т. Д.), Либо сделать свой собственный typedef, который указывает на функцию данного тип (prng_int, adc_int и т. д.).

helloworld922
источник
0

Часто код используется в ARM thumb и AVR (а также в x86, powerPC и других архитектурах), а 16 или 32-разрядная версия может быть более эффективной (в обоих случаях: флэш-память и циклы) в ARM STM32 даже для переменной, которая умещается в 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 немного помогает, но обычно определения типов имеют смысл:

typedef uint_least8_t uintG8_t;
typedef uint_fast8_t uintL8_t;

// но uint_fast8_t для ОБА, когда SRAM не имеет значения.

Marcell
источник