У меня есть неопределенное поведение в кажущейся безобидной функции, которая анализирует double
значение из буфера. Я читаю double
в две половинки, потому что я достаточно уверен, что языковой стандарт говорит, что сдвиг значений char
действителен только в 32-битном контексте.
inline double ReadLittleEndianDouble( const unsigned char *buf )
{
uint64_t lo = (buf[3] << 24) | (buf[2] << 16) | (buf[1] << 8) | buf[0];
uint64_t hi = (buf[7] << 24) | (buf[6] << 16) | (buf[5] << 8) | buf[4];
uint64_t val = (hi << 32) | lo;
return *(double*)&val;
}
Поскольку я храню 32-битные значения в 64-битных переменных lo
и hi
, я разумно ожидаю, что 32-разрядные значения этих переменных будут всегда 0x00000000
. Но иногда они содержат 0xffffffff
или другой 0xffffffff
мусор.
Исправление состоит в том, чтобы замаскировать его следующим образом:
uint64_t val = ((hi & 0xffffffffULL) << 32) | (lo & 0xffffffffULL);
Кроме того, он работает, если я маскирую во время назначения вместо этого:
uint64_t lo = ((buf[3] << 24) | (buf[2] << 16) | (buf[1] << 8) | buf[0]) & 0xffffffff;
uint64_t hi = ((buf[7] << 24) | (buf[6] << 16) | (buf[5] << 8) | buf[4]) & 0xffffffff;
Я хотел бы знать, почему это необходимо. Все, что я могу придумать, чтобы объяснить это, заключается в том, что мой компилятор делает все смещение и объединение для lo
и hi
непосредственно на 64-битных регистрах, и я мог бы ожидать неопределенного поведения в 32-битных разрядах высокого порядка, если это так.
Может кто-то, пожалуйста, подтвердите мои подозрения или иным образом объясните, что здесь происходит, и прокомментируйте, какой (если есть) из моих двух решений предпочтительнее?
Если вы попытаетесь перенести char
или unsigned char
вы оставите себя во власти стандартных целых рекламных акций. Вам лучше отказаться от ценностей самостоятельно, прежде чем пытаться их переместить. Если вы это сделаете, вам не нужно отделять нижнюю и верхнюю половинки.
inline double ReadLittleEndianDouble( const unsigned char *buf )
{
uint64_t val = ((uint64_t)buf[7] << 56) | ((uint64_t)buf[6] << 48) | ((uint64_t)buf[5] << 40) | ((uint64_t)buf[4] << 32) |
((uint64_t)buf[3] << 24) | ((uint64_t)buf[2] << 16) | ((uint64_t)buf[1] << 8) | (uint64_t)buf[0];
return *(double*)&val;
}
Все это необходимо только в том случае, если процессор имеет большой энтитизм или если буфер не может быть правильно выровнен для архитектуры ЦП, в противном случае вы можете значительно упростить это:
return *(double*)buf;
*(double *)buf
нарушает строгое правило алиасинга
<<
не применяется кchar
операнду. Целочисленные преобразования применяются к операндам, поэтомуchar
повышается доint
(или, возможно, доunsigned int
в действительно странной реализации). Правый операнд (не более 24) не «больше или равен числу битов в расширенном левом операнде»; проблема в том, чтоfoo << 24
может переполниться. Приведения кunsigned
должно быть достаточно, еслиint
иunsigned int
имеют ширину не менее 32 бит.uint64_t
кuint64_t
, вероятно, чище.bswap
а другие - нет. Я не мог размышлять о том, что испускает компилятор все же.