Перейти к содержанию

Целочисленное продвижение (integer promotion)

В языке C арифметика не выполняется над типами меньше int. Перед любой операцией переменные uint8_t, char и short превращаются в int — это целочисленное продвижение. Оно незаметно, пока не столкнёшься с инверсией байта или сдвигом на AVR, где int всего 16 бит.

Обновлено Редакция GAW

Целочисленное продвижение (integer promotion) — правило языка C, по которому перед вычислением выражения операнды целых типов с рангом ниже int — char, short, uint8_t, int8_t, uint16_t на 32-битных платформах, битовые поля — заменяются на int, если он вмещает все их значения, иначе на unsigned int (C11, 6.3.1.1). Арифметика, сравнения и сдвиги выполняются уже над продвинутыми значениями.

Размер int решает всё

Платформаintuint8_t продвигается вuint16_t продвигается в
AVR (avr-gcc)16 битintостаётся unsigned int (это и есть его тип)
Cortex-M (32 бит)32 битаintint

Размеры типов avr-gcc — по GCC Wiki: short и int по 2 байта. Поэтому одно и то же выражение может вести себя по-разному на AVR и на STM32.

Три ловушки

  1. Инверсия байта. uint8_t x = 0x0F; ~x — это не 0xF0, а 0xFFF0 на AVR и 0xFFFFFFF0 на Cortex-M, потому что инвертируется продвинутый int. Сравнение ~x == 0xF0 ложно; нужно (uint8_t)~x.
  2. Сдвиг байта на AVR. x << 8 при x = 0x80 вычисляется в 16-битном знаковом int: 32 768 не помещается, и по C11 (6.5.7) это неопределённое поведение. Пишут (uint16_t)x << 8. То же на Cortex-M с x << 24.
  3. Сумма без переполнения. uint8_t a = 200, b = 100; выражение a + b равно 300 — переполнения нет, пока результат не записали обратно в uint8_t, где он станет 44 по модулю 256.

Числа в примерах — наш расчёт по правилам C11.

Как жить с продвижением

Явно приводить результат к нужному типу при инверсии и сравнении, приводить левый операнд сдвига к беззнаковому типу нужной ширины, следить за знаковостью при смешении int и unsigned int.

Не путать продвижение с обычными арифметическими преобразованиями: продвижение поднимает малые типы до int, а обычные преобразования затем приводят два операнда к общему типу, например int и long — к long. Подробнее о типах на AVR и ARM — в статье «Язык C для микроконтроллеров», о сдвигах и масках — в статье «Битовые операции в C», поле регистра и код маски сгенерирует калькулятор битовых масок.

Частые вопросы

Почему ~x для uint8_t x = 0x0F не равно 0xF0?

Перед инверсией x продвигается до int, и инвертируются все биты int: на 16-битном int AVR получается 0xFFF0, на 32-битном Cortex-M — 0xFFFFFFF0 (как число со знаком это −16). Сравнение ~x == 0xF0 ложно. Нужно привести результат обратно: (uint8_t)~x == 0xF0.

Переполнится ли сумма двух uint8_t?

Нет, если результат хранить в типе пошире: uint8_t a = 200, b = 100; сумма a + b вычисляется в int и равна 300. Переполнение «заворачиванием» до 44 произойдёт только при записи результата обратно в uint8_t — по C11 это преобразование в беззнаковый тип по модулю 256.

Чем опасен сдвиг байта на AVR?

uint8_t x = 0x80; выражение x << 8 вычисляется в int, а int на AVR 16-битный и знаковый. Результат 0x8000 = 32 768 в 16-битный int не помещается, и по C11 (6.5.7) это неопределённое поведение. Правильно сначала привести байт к беззнаковому типу нужной ширины: (uint16_t)x << 8 или (uint32_t)x << 24.

Источники

  1. ISO/IEC 9899:201x (C11), Committee Draft N1570, 2011 — 6.3.1.1: ранг целых типов; объект с рангом не выше int заменяется на int, если int вмещает все его значения, иначе на unsigned int — это целочисленные продвижения; 6.5.7: продвижения выполняются над операндами сдвига, тип результата — продвинутый левый операнд; сдвиг знакового E1, результат которого не помещается, — неопределённое поведение; 6.3.1.3: преобразование в беззнаковый тип — по модулю
  2. GCC Wiki: avr-gcc (ABI, Type Layout) — в avr-gcc char — 1 байт, short и int — 2 байта, long — 4, long long — 8

Нашли ошибку?