В этом ответе , zwol сделал это заявление:
Правильный способ преобразования двух байтов данных из внешнего источника в 16-разрядное целое число со знаком - с помощью вспомогательных функций, таких как:
#include <stdint.h>
int16_t be16_to_cpu_signed(const uint8_t data[static 2]) {
uint32_t val = (((uint32_t)data[0]) << 8) |
(((uint32_t)data[1]) << 0);
return ((int32_t) val) - 0x10000u;
}
int16_t le16_to_cpu_signed(const uint8_t data[static 2]) {
uint32_t val = (((uint32_t)data[0]) << 0) |
(((uint32_t)data[1]) << 8);
return ((int32_t) val) - 0x10000u;
}
Какая из вышеуказанных функций подходит, зависит от того, содержит ли массив представление с прямым или обратным порядком байтов. Порядок байтов не является проблемой на вопрос здесь, я задаюсь вопросом, почему zwol вычитает 0x10000u
из uint32_t
значения преобразуются в int32_t
.
Почему это правильный путь ?
Как избежать поведения, определенного при реализации, при преобразовании в тип возвращаемого значения?
Так как вы можете предположить представление дополнения 2, как это простое приведение завершится неудачно: return (uint16_t)val;
Что не так с этим наивным решением:
int16_t le16_to_cpu_signed(const uint8_t data[static 2]) {
return (uint16_t)data[0] | ((uint16_t)data[1] << 8);
}
источник
int16_t
зависит от реализации, поэтому наивный подход не переносим.int16_t
0xFFFF0001u
не может быть представлен какint16_t
, а во втором подходе0xFFFFu
не может быть представлен какint16_t
.Ответы:
Если
int
16-битный, то ваша версия опирается на поведение, определяемое реализацией, если значение выражения вreturn
выражении выходит за пределы диапазонаint16_t
.Однако первая версия также имеет аналогичную проблему; например, если
int32_t
typedef дляint
, а входные байты оба0xFF
, то результатом вычитания в операторе return является то,UINT_MAX
что вызывает поведение, определяемое реализацией, при преобразовании вint16_t
.ИМХО ответ, на который вы ссылаетесь, имеет несколько основных проблем.
источник
int16_t
?uchar8_t
.Это должно быть педантично правильным и работать также на платформах, которые используют знаковый бит или представления дополнения 1 вместо обычного дополнения 2 . Предполагается, что входные байты находятся в дополнении 2.
Из-за ветки, это будет дороже, чем другие варианты.
Это позволяет избежать каких-либо предположений о том, как
int
представление относится кunsigned
представлению на платформе. Приведение кint
требуется для сохранения арифметического значения для любого числа, которое будет соответствовать целевому типу. Поскольку инверсия гарантирует, что старший бит 16-разрядного числа будет равен нулю, значение будет соответствовать. Тогда унарное-
и вычитание 1 применяют обычное правило для отрицания дополнения 2. В зависимости от платформыINT16_MIN
может все еще переполниться, если он не соответствуетint
типу на цели, и в этом случаеlong
следует использовать.Разница с оригинальной версией в вопросе приходит во время возврата. В то время как оригинал всегда всегда вычитался,
0x10000
а дополнение 2 позволяло знаменному переполнению переносить его вint16_t
диапазон, в этой версии есть явное,if
которое избегает подписанного переноса (который не определен ).Сейчас на практике почти все платформы, используемые сегодня, используют представление дополнения 2. Фактически, если у платформы есть совместимый со стандартом,
stdint.h
который определяетint32_t
, это должно использовать дополнение 2 для этого. Иногда этот подход оказывается полезным при использовании некоторых языков сценариев, которые вообще не имеют целочисленных типов данных - вы можете изменить операции, показанные выше для чисел с плавающей запятой, и это даст правильный результат.источник
int16_t
и любой,intxx_t
и их беззнаковые варианты должны использовать представление дополнения 2 без битов заполнения. Для размещения этих типов и использования другого представления потребовалась бы целенаправленная извращенная архитектураint
, но я предполагаю, что DS9K можно настроить таким образом.int
чтобы избежать путаницы. Действительно, если платформа определяет,int32_t
это должно быть дополнение 2.intN_t
обозначает целочисленный тип соN
int8_t
знаком с шириной , без битов заполнения и представление дополнения до двух. Таким образом, обозначает целочисленный тип со знаком шириной ровно 8 бит. Другие представления все еще поддерживаются стандартом, но для других целочисленных типов.(int)value
имеет поведение, определяемое реализацией, если типint
имеет только 16 бит. Я боюсь, что вам нужно использовать(long)value - 0x10000
, но на архитектурах дополнения не 2, значение0x8000 - 0x10000
не может быть представлено как 16-битныйint
, поэтому проблема остается.long
будет работать одинаково хорошо.Другой метод - использование
union
:В программе:
first_byte
иsecond_byte
может быть заменен в соответствии с маленькой или большой байтовой моделью. Этот метод не лучше, но является одной из альтернатив.источник
byte[2]
иint16_t
имеют тот же размер, что один или другой из двух возможных порядков, а не какие - то произвольные перемешиваются битовые значений места. Таким образом, вы можете, по крайней мере, определить во время компиляции, какой порядковый номер имеет реализация.Арифметические операторы shift и bitwise-or в выражении
(uint16_t)data[0] | ((uint16_t)data[1] << 8)
не работают с типами, меньшими чемint
, так что этиuint16_t
значения повышаются доint
(илиunsigned
ifsizeof(uint16_t) == sizeof(int)
). Тем не менее, это должно дать правильный ответ, так как только младшие 2 байта содержат значение.Еще одна педантически правильная версия для преобразования с прямым порядком байтов в младший (если предполагается, что процессоры с прямым порядком байтов):
memcpy
используется для копирования представления,int16_t
и это является стандартным способом сделать это. Эта версия также компилируется в 1 инструкциюmovbe
, см. Сборку .источник
__builtin_bswap16
заключается в том, что замена байтов в ISO C не может быть реализована так эффективно.int16_t
вuint16_t
хорошо определено: отрицательные значения преобразуются в значения больше, чемINT_MAX
, но преобразование этих значений обратноuint16_t
является поведением, определяемым реализацией: 6.3.1.3 Целые числа со знаком и без знака 1. Когда значение с целочисленным типом преобразуется в другой целочисленный тип, отличный от _Bool, если значение может быть представлено новым типом, оно не изменяется. ... 3. В противном случае новый тип подписывается и значение не может быть представлено в нем; либо результат определяется реализацией, либо определяется сигнал реализации.ntohs
/__builtin_bswap
и|
/<<
pattern: gcc.godbolt.org/z/rJ-j87Вот еще одна версия, которая опирается только на переносимое и четко определенное поведение (заголовок
#include <endian.h>
не стандартный, код такой):Версия с прямым порядком байтов компилируется в одну
movbe
инструкциюclang
,gcc
версия менее оптимальна, см. Сборку .источник
uint16_t
вint16_t
конверсии, в этой версии нет конверсии, так что вы идете.Я хочу поблагодарить всех авторов за их ответы. Вот к чему сводится коллективная работа:
uint8_t
,int16_t
иuint16_t
должен использовать комплемент представление двоичного без каких - либо бит заполнения, так что фактические биты представления однозначно являются те , из 2 -х байт в массиве, в порядке , определенном имена функций.(unsigned)data[0] | ((unsigned)data[1] << 8)
(для версии с прямым порядком байтов) компилируется в одну инструкцию и дает 16-разрядное значение без знака.uint16_t
в тип со знакомint16_t
имеет поведение, определяемое реализацией, если значение не находится в диапазоне типа назначения. Специальных положений для типов, представление которых точно определено, не предусмотрено.INT_MAX
и вычислить соответствующее значение со знаком путем вычитания0x10000
. Выполнение этого для всех значений, как предложено zwol, может привести к значениям вне диапазонаint16_t
с таким же поведением, определенным реализацией.0x8000
бит явно приводит к тому, что компиляторы создают неэффективный код.memcpy
.Комбинируя пункты 2 и 7, вот переносимое и полностью определенное решение, которое эффективно компилируется в одну инструкцию с использованием gcc и clang :
64-битная сборка :
источник
char
типы могут создавать псевдонимы или содержать объектные представления любого другого типа.uint16_t
не один изchar
видов, так чтоmemcpy
изuint16_t
кint16_t
не вполне определенное поведение. Стандарт требует толькоchar[sizeof(T)] -> T > char[sizeof(T)]
преобразования с,memcpy
чтобы быть четко определенным.memcpy
изuint16_t
к конкретнойint16_t
реализации определяется в лучшем случае , не переносимы, а не четко определены, точно так , как присвоение одного к другому, и вы не можете волшебным образом обойти , что сmemcpy
. Неважно,uint16_t
использует ли представление дополнения два или нет, или присутствуют биты заполнения или нет - это не определяется поведением или не требуется стандартом Си.r = u
наmemcpy(&r, &u, sizeof u)
но последний не лучше , чем первый, это?