C++ 中从 24 位到 32 位的签名扩展
Signed extension from 24 bit to 32 bit in C++
我有 3 个无符号字节分别通过网络传输。
[byte1, byte2, byte3]
我需要将它们转换为带符号的 32 位值,但我不太确定如何处理负值的符号。
我想将字节复制到 int32 中的高 3 个字节,然后将所有内容向右移动,但我读到这可能会出现意外行为。
有没有更简单的方法来处理这个问题?
用二进制补码表示。
假设两种表示都是二进制补码,简单地
upper_byte = (Signed_byte(incoming_msb) >= 0? 0 : Byte(-1));
哪里
using Signed_byte = signed char;
using Byte = unsigned char;
和upper_byte
是一个变量,表示缺少的第四个字节。
到 Signed_byte
的转换在形式上取决于实现,但二进制补码实现并没有选择,真的。
您可以使用 bitfield
template<size_t L>
inline int32_t sign_extend_to_32(const char *x)
{
struct {int32_t i: L;} s;
memcpy(&s, x, 3);
return s.i;
// or
return s.i = (x[2] << 16) | (x[1] << 8) | x[0]; // assume little endian
}
简单且没有调用未定义的行为
int32_t r = sign_extend_to_32<24>(your_3byte_array);
当然将字节复制到 int32 的高 3 字节,然后将所有内容向右移动也是一个好主意。如果您像上面那样使用 memcpy
,则没有未定义的行为。另一种方法是 C++ 中的 reinterpret_cast
和 C 中的 union,可以避免使用 memcpy
。但是有一个 implementation defined behavior 因为右移并不总是符号扩展移位(尽管几乎所有现代编译器都这样做)
您可以让编译器自行处理符号扩展。假设最低有效字节为byte1,高有效字节为byte3;
int val = (signed char) byte3; // C guarantees the sign extension
val << 16; // shift the byte at its definitive place
val |= ((int) (unsigned char) byte2) << 8; // place the second byte
val |= ((int) (unsigned char) byte1; // and the least significant one
当 static_cast
更像 C++ 时,我在这里使用了 C 风格转换,但作为一个老恐龙(和 Java 程序员)我发现 C 风格转换对于整数更具可读性转换。
这是一种适用于任何位数的方法,即使它不是 8 的倍数。这里假设您已经将 3 个字节组装成一个整数 value
。
const int bits = 24;
int mask = (1 << bits) - 1;
bool is_negative = (value & ~(mask >> 1)) != 0;
value |= -is_negative & ~mask;
您可以使用:
uint32_t sign_extend_24_32(uint32_t x) {
const int bits = 24;
uint32_t m = 1u << (bits - 1);
return (x ^ m) - m;
}
之所以有效,是因为:
- 如果旧符号为 1,则 XOR 将其变为零,减法将设置它并借用所有更高位,同时设置它们。
- 如果旧符号为 0,则 XOR 将设置它,减法再次重置它并且不借位,因此高位保持为 0。
模板化版本
template<class T>
T sign_extend(T x, const int bits) {
T m = 1;
m <<= bits - 1;
return (x ^ m) - m;
}
这是一个很老的问题,但我最近不得不做同样的事情(在处理 24 位音频样本时),并为此编写了我自己的解决方案。它使用与 答案类似的原理,但更通用,并且可能在编译后生成更好的代码。
template <size_t Bits, typename T>
inline constexpr T sign_extend(const T& v) noexcept {
static_assert(std::is_integral<T>::value, "T is not integral");
static_assert((sizeof(T) * 8u) >= Bits, "T is smaller than the specified width");
if constexpr ((sizeof(T) * 8u) == Bits) return v;
else {
using S = struct { signed Val : Bits; };
return reinterpret_cast<const S*>(&v)->Val;
}
}
这没有硬编码数学,它只是让编译器完成工作并找出对数字进行符号扩展的最佳方法。对于某些宽度,这甚至可以在程序集中生成本机符号扩展指令,例如 x86 上的 MOVSX。
此函数假定您已将 N 位数复制到要将其扩展到的类型的低 N 位。例如:
int16_t a = -42;
int32_t b{};
memcpy(&b, &a, sizeof(a));
b = sign_extend<16>(b);
当然它适用于任意数量的位,将其扩展到包含数据的类型的整个宽度。
假设您的 24 位值存储在变量 int32_t val 中,您可以通过以下方式轻松扩展符号:
val = (val << 8) >> 8;
我有 3 个无符号字节分别通过网络传输。
[byte1, byte2, byte3]
我需要将它们转换为带符号的 32 位值,但我不太确定如何处理负值的符号。
我想将字节复制到 int32 中的高 3 个字节,然后将所有内容向右移动,但我读到这可能会出现意外行为。
有没有更简单的方法来处理这个问题?
用二进制补码表示。
假设两种表示都是二进制补码,简单地
upper_byte = (Signed_byte(incoming_msb) >= 0? 0 : Byte(-1));
哪里
using Signed_byte = signed char;
using Byte = unsigned char;
和upper_byte
是一个变量,表示缺少的第四个字节。
到 Signed_byte
的转换在形式上取决于实现,但二进制补码实现并没有选择,真的。
您可以使用 bitfield
template<size_t L>
inline int32_t sign_extend_to_32(const char *x)
{
struct {int32_t i: L;} s;
memcpy(&s, x, 3);
return s.i;
// or
return s.i = (x[2] << 16) | (x[1] << 8) | x[0]; // assume little endian
}
简单且没有调用未定义的行为
int32_t r = sign_extend_to_32<24>(your_3byte_array);
当然将字节复制到 int32 的高 3 字节,然后将所有内容向右移动也是一个好主意。如果您像上面那样使用 memcpy
,则没有未定义的行为。另一种方法是 C++ 中的 reinterpret_cast
和 C 中的 union,可以避免使用 memcpy
。但是有一个 implementation defined behavior 因为右移并不总是符号扩展移位(尽管几乎所有现代编译器都这样做)
您可以让编译器自行处理符号扩展。假设最低有效字节为byte1,高有效字节为byte3;
int val = (signed char) byte3; // C guarantees the sign extension
val << 16; // shift the byte at its definitive place
val |= ((int) (unsigned char) byte2) << 8; // place the second byte
val |= ((int) (unsigned char) byte1; // and the least significant one
当 static_cast
更像 C++ 时,我在这里使用了 C 风格转换,但作为一个老恐龙(和 Java 程序员)我发现 C 风格转换对于整数更具可读性转换。
这是一种适用于任何位数的方法,即使它不是 8 的倍数。这里假设您已经将 3 个字节组装成一个整数 value
。
const int bits = 24;
int mask = (1 << bits) - 1;
bool is_negative = (value & ~(mask >> 1)) != 0;
value |= -is_negative & ~mask;
您可以使用:
uint32_t sign_extend_24_32(uint32_t x) {
const int bits = 24;
uint32_t m = 1u << (bits - 1);
return (x ^ m) - m;
}
之所以有效,是因为:
- 如果旧符号为 1,则 XOR 将其变为零,减法将设置它并借用所有更高位,同时设置它们。
- 如果旧符号为 0,则 XOR 将设置它,减法再次重置它并且不借位,因此高位保持为 0。
模板化版本
template<class T>
T sign_extend(T x, const int bits) {
T m = 1;
m <<= bits - 1;
return (x ^ m) - m;
}
这是一个很老的问题,但我最近不得不做同样的事情(在处理 24 位音频样本时),并为此编写了我自己的解决方案。它使用与
template <size_t Bits, typename T>
inline constexpr T sign_extend(const T& v) noexcept {
static_assert(std::is_integral<T>::value, "T is not integral");
static_assert((sizeof(T) * 8u) >= Bits, "T is smaller than the specified width");
if constexpr ((sizeof(T) * 8u) == Bits) return v;
else {
using S = struct { signed Val : Bits; };
return reinterpret_cast<const S*>(&v)->Val;
}
}
这没有硬编码数学,它只是让编译器完成工作并找出对数字进行符号扩展的最佳方法。对于某些宽度,这甚至可以在程序集中生成本机符号扩展指令,例如 x86 上的 MOVSX。
此函数假定您已将 N 位数复制到要将其扩展到的类型的低 N 位。例如:
int16_t a = -42;
int32_t b{};
memcpy(&b, &a, sizeof(a));
b = sign_extend<16>(b);
当然它适用于任意数量的位,将其扩展到包含数据的类型的整个宽度。
假设您的 24 位值存储在变量 int32_t val 中,您可以通过以下方式轻松扩展符号:
val = (val << 8) >> 8;