Каков самый безопасный кросс-платформенный способ получить младший или старший байт 16-битного целого числа?

avatar
jestro
1 июля 2021 в 21:23
152
1
1

При просмотре различных пакетов SDK кажется, что LOBYTE и HIBYTE редко совпадают, как показано ниже.

Windows

#define LOBYTE(w)           ((BYTE)(((DWORD_PTR)(w)) & 0xff))
#define HIBYTE(w)           ((BYTE)((((DWORD_PTR)(w)) >> 8) & 0xff))

Различные заголовки Linux

#define HIBYTE(w)       ((u8)(((u16)(w) >> 8) & 0xff))
#define LOBYTE(w)       ((u8)(w))

Зачем нужен & 0xff, если он приведен к u8? Почему бы не пойти по следующему пути? (при условии, что uint8_t и uint16_t определены)

#define HIBYTE(w)       ((uint8_t)(((uint16_t)(w) >> 8)))
#define LOBYTE(w)       ((uint8_t)(w))
Источник
Karl Knechtel
1 июля 2021 в 21:27
1

Добавлено несколько тегов, потому что, если это не о том, как избежать каких-либо скрытых ошибок, это слишком субъективно для переполнения стека.

Nate Eldredge
1 июля 2021 в 21:29
2

Обратите внимание, что uint8_t, uint16_t и т. д. являются необязательными; реализация не обязана даже иметь целые типы этих конкретных ширин. (Например, unsigned char не обязательно должен быть 8-битным, и существуют системы, где он 9-битный или 32-битный.) Поэтому их нельзя использовать в действительно «кросс-платформенном» решении.

jestro
1 июля 2021 в 21:33
0

@NateEldredge Предположим, что они определены.

chux - Reinstate Monica
1 июля 2021 в 21:41
1

"Зачем нужен &0xff, если он залит на u8?" --> Пояс и подтяжки. Предпочтителен только (uint8_t), так как он определяет тип.

Andrew Henle
1 июля 2021 в 22:32
0

При рассмотрении различных пакетов SDK кажется, что LOBYTE и HIBYTE редко бывают совместимыми, как показано ниже. На самом деле они логически идентичны и, следовательно, непротиворечивы. Младший байт всегда представляет собой полное 16-битное значение, усеченное до младших 8 бит. Старший байт всегда представляет собой полное 16-битное значение, сдвинутое вправо на 8 бит, а затем усеченное до младших 8 бит.

David C. Rankin
1 июля 2021 в 22:45
1

Обратите внимание, что u8 и u16 являются дополнительными необязательными определениями типов для необязательных uint8_t и uint16_t. uint8_t и uint16_t с гораздо большей вероятностью будут предоставлены через заголовок stdint.h. Учитывая все три ваших примера, наиболее переносимым является вариант с uint8_t и uint16_t.

Ответы (1)

avatar
nilo
1 июля 2021 в 21:51
4

Из ISO/IEC 9899:TC3, 6.3.1.3 Целые числа со знаком и без знака (согласно 6.3 Преобразования):

  1. Когда значение целочисленного типа преобразуется в другой целочисленный тип, отличный от _Bool, если значение может быть представлено новым типом, оно не изменился.
  2. В противном случае, если новый тип не имеет знака, значение преобразуется путем многократного добавления или вычитания на единицу больше, чем максимальное значение, которое может быть представлен в новом типе до тех пор, пока значение не окажется в диапазоне новый тип.

Хотя это звучит немного запутанно, это отвечает на следующий вопрос.

Зачем нужен & 0xff, если он преобразован в u8?

В этом нет необходимости, поскольку приведение автоматически выполняет маскирование.

Когда дело доходит до вопроса в теме, последнее предложение ОП:

#define HIBYTE(w)       ((uint8_t)(((uint16_t)(w) >> 8)))
#define LOBYTE(w)       ((uint8_t)(w))

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

Реализовать переносимое решение для целых чисел со знаком было бы довольно сложно, и можно даже поставить под сомнение смысл такой реализации:

  • Должен ли результат быть подписанным или беззнаковым?
  • Если результат должен быть беззнаковым, он на самом деле не квалифицируется как старший/младший байт исходного числа, поскольку для его получения может потребоваться изменение представления.
  • Если результат должен быть подписан, это должно быть дополнительно указано. Результат >> для отрицательных значений, например, определяется реализацией, поэтому получение переносимого четко определенного «старшего байта» кажется сложной задачей. Следует действительно задаться вопросом о цели такого расчета.

И так как мы играем в юриста по языку, мы могли бы задаться вопросом о подписи левого операнда (uint16_t)(w) >> 8. Unsigned может показаться очевидным ответом, но это не так из-за правил целочисленного продвижения.

Целое преобразование применяется, среди прочего, к объектам или выражениям, указанным ниже.

Объект или выражение с целочисленным типом, ранг целочисленного преобразования которого меньше или равен рангу int и unsigned int.

Правило целочисленного повышения в таком случае указывается как:

Если целое число может представлять все значения исходного типа, значение преобразуется в целое число;

Это будет иметь место для левого операнда на типичной 32-битной или 64-битной машине.

К счастью, в таком случае левый операнд после преобразования по-прежнему будет неотрицательным, что делает результат >> вполне определенным:

Результатом E1 >> E2 является бит E2 со сдвигом вправо в E1. Если E1 имеет тип без знака или если E1 имеет тип со знаком и неотрицательное значение, значение результата является целой частью частного E1 / 2E2.

0___________
1 июля 2021 в 22:06
0

Но это хорошо для человека. Компиляторы не делают