如何在没有未定义行为的情况下安全地偏移位?
How to safely offset bits without undefined behaviour?
考虑到位集的位数可能少于目标类型,我正在编写一个将位集转换为 int/uint 值的函数。
这是我写的函数:
template <typename T,size_t count> static T convertBitSetToNumber( const std::bitset<count>& bitset )
{
T result;
#define targetSize (sizeof( T )*CHAR_BIT)
if ( targetSize > count )
{
// if bitset is 0xF00, converting it as 0x0F00 will lose sign information (0xF00 is negative, while 0x0F00 is positive)
// This is because sign bit is on the left.
// then, we need to add a zero (4bits) on the right and then convert 0xF000, later, we will divide by 16 (2^4) to preserve sign and value
size_t missingbits = targetSize - count;
std::bitset<targetSize> extended;
extended.reset(); // set all to 0
for ( size_t i = 0; i != count; ++i )
{
if ( i < count )
extended[i+missingbits] = bitset[i];
}
result = static_cast<T>( extended.to_ullong() );
result = result >> missingbits;
return result;
}
else
{
return static_cast<T>( bitset.to_ullong() );
}
}
和 "test program":
uint16_t val1 = Base::BitsetUtl::convertBitSetToNumber<uint16_t,12>( std::bitset<12>( "100010011010" ) );
// val1 is 0x089A
int16_t val2 = Base::BitsetUtl::convertBitSetToNumber<int16_t,12>( std::bitset<12>( "100010011010" ) );
// val2 is 0xF89A
注意: 参见 comment/exchange 与 Ped7g,他上面的代码是正确的并且保留位符号并且对有符号或无符号位进行 12->16 位转换。但是,如果您正在寻找如何将签名对象上的 0xABC0 偏移到 0x0ABC,答案可能会对您有所帮助,所以我不会删除该问题。
查看程序在使用 uint16
作为目标类型时运行,如:
uint16_t val = 0x89A0; // 1000100110100000
val = val >> 4; // 0000100010011010
但是,使用 int16_t
时失败,因为 0x89A0 >> 4
是 0xF89A
而不是预期的 0x089A
.
int16_t val = 0x89A0; // 1000100110100000
val = val >> 4; // 1111100010011010
我不明白为什么 >> 运算符有时插入 0 有时插入 1。而且我无法找到如何安全地执行函数的最终操作(result = result >> missingbits;
一定是在某些时候出错了...)
这叫做arithmetic shifting。在有符号类型上,最高有效位是符号位。当您将负值右移时,高位设置为 1,这样结果仍然是负数。 (结果是除以 2n,其中 n 是移位的位数,向负无穷大四舍五入)。
为避免这种情况,请使用无符号类型。移动它们使用 logical shifting,这会将高位设置为 0。
更改此行:
result = result >> missingbits;
到
result = static_cast<T>(static_cast<uintmax_t>(result) >> missingbits);
(uintmax_t
是编译器支持的最大宽度无符号整数类型)
或使用 std::make_unsigned
正如 Joachim Pileborg 在他的回答中所写。
因为移位是算术运算,将操作数提升为int
,会做符号扩展
即将带符号的 16 位整数 (int16_t
) 0x89a0
提升为 32 位带符号整数 (int
) 会导致值变为 0xffff89a0
,即转移了。
参见例如this arithmetic operation conversion reference 获取更多信息。
您应该将变量(或值)转换为无符号整数(即在您的情况下为 uint16_t
):
val = static_cast<uint16_t>(val) >> 4;
如果不知道类型,比如它是一个模板参数,那么你可以使用 std::make_unsigned
:
val = static_cast<typename std::make_unsigned<T>::type>(val) >> 4;
如前所述,当您的类型为 signed
时,>>
运算符正在执行算术移位。因此,除了上面建议的解决方案之外,如果您需要进行逻辑移位,您总是可以简单地使用 mask
如下所示:
int mask = 1 << (targetSize-missingbits-1);
mask |= mask - 1;
result = (result >> missingbits) & mask;
在这种情况下,mask
将为您提供 missingbits
MSB 的 0
和其余的 1
。在您的情况下,4 个 MSB 为 0
,其余为 1
。然后,执行 &
操作将重置 result
中的第一个 missingbits
,这就是您需要的:
0xF89A & 0x0FFF = 0x089A
看到它工作 live-example。
那个带有循环的原始代码对我来说看起来有点复杂,我会这样写(我的意思是作为第二个选项,在我莫名其妙地无法避免使用 std::bitset
和模板之后完全,对于数据的位大小调整这样简单的事情,首先):
#include <bitset>
#include <climits>
template <typename T,size_t count> static T convertBitSetToNumber( const std::bitset<count>& bitset )
{
constexpr size_t targetSize = sizeof( T )*CHAR_BIT;
if (targetSize == count) return static_cast<T>(bitset.to_ullong());
if (targetSize < count) return static_cast<T>(bitset.to_ullong() >> (count - targetSize));
return static_cast<T>(bitset.to_ullong() << (targetSize - count)) >> (targetSize - count);
}
// Example test producing from 0x089A bitset unsigned/signed values:
// 16b: 89a f89a | 8b: 89 89 | 32b: 89a fffff89a
#include <iostream>
int main()
{
const std::bitset<12> testBitset("100010011010");
std::hex(std::cout);
std::cout << convertBitSetToNumber<uint16_t,12>( testBitset ) << std::endl;
std::cout << convertBitSetToNumber<int16_t,12>( testBitset ) << std::endl;
std::cout << (0xFF & convertBitSetToNumber<uint8_t,12>( testBitset )) << std::endl;
std::cout << (0xFF & convertBitSetToNumber<int8_t,12>( testBitset )) << std::endl;
std::cout << convertBitSetToNumber<uint32_t,12>( testBitset ) << std::endl;
std::cout << convertBitSetToNumber<int32_t,12>( testBitset ) << std::endl;
}
考虑到位集的位数可能少于目标类型,我正在编写一个将位集转换为 int/uint 值的函数。
这是我写的函数:
template <typename T,size_t count> static T convertBitSetToNumber( const std::bitset<count>& bitset )
{
T result;
#define targetSize (sizeof( T )*CHAR_BIT)
if ( targetSize > count )
{
// if bitset is 0xF00, converting it as 0x0F00 will lose sign information (0xF00 is negative, while 0x0F00 is positive)
// This is because sign bit is on the left.
// then, we need to add a zero (4bits) on the right and then convert 0xF000, later, we will divide by 16 (2^4) to preserve sign and value
size_t missingbits = targetSize - count;
std::bitset<targetSize> extended;
extended.reset(); // set all to 0
for ( size_t i = 0; i != count; ++i )
{
if ( i < count )
extended[i+missingbits] = bitset[i];
}
result = static_cast<T>( extended.to_ullong() );
result = result >> missingbits;
return result;
}
else
{
return static_cast<T>( bitset.to_ullong() );
}
}
和 "test program":
uint16_t val1 = Base::BitsetUtl::convertBitSetToNumber<uint16_t,12>( std::bitset<12>( "100010011010" ) );
// val1 is 0x089A
int16_t val2 = Base::BitsetUtl::convertBitSetToNumber<int16_t,12>( std::bitset<12>( "100010011010" ) );
// val2 is 0xF89A
注意: 参见 comment/exchange 与 Ped7g,他上面的代码是正确的并且保留位符号并且对有符号或无符号位进行 12->16 位转换。但是,如果您正在寻找如何将签名对象上的 0xABC0 偏移到 0x0ABC,答案可能会对您有所帮助,所以我不会删除该问题。
查看程序在使用 uint16
作为目标类型时运行,如:
uint16_t val = 0x89A0; // 1000100110100000
val = val >> 4; // 0000100010011010
但是,使用 int16_t
时失败,因为 0x89A0 >> 4
是 0xF89A
而不是预期的 0x089A
.
int16_t val = 0x89A0; // 1000100110100000
val = val >> 4; // 1111100010011010
我不明白为什么 >> 运算符有时插入 0 有时插入 1。而且我无法找到如何安全地执行函数的最终操作(result = result >> missingbits;
一定是在某些时候出错了...)
这叫做arithmetic shifting。在有符号类型上,最高有效位是符号位。当您将负值右移时,高位设置为 1,这样结果仍然是负数。 (结果是除以 2n,其中 n 是移位的位数,向负无穷大四舍五入)。
为避免这种情况,请使用无符号类型。移动它们使用 logical shifting,这会将高位设置为 0。
更改此行:
result = result >> missingbits;
到
result = static_cast<T>(static_cast<uintmax_t>(result) >> missingbits);
(uintmax_t
是编译器支持的最大宽度无符号整数类型)
或使用 std::make_unsigned
正如 Joachim Pileborg 在他的回答中所写。
因为移位是算术运算,将操作数提升为int
,会做符号扩展
即将带符号的 16 位整数 (int16_t
) 0x89a0
提升为 32 位带符号整数 (int
) 会导致值变为 0xffff89a0
,即转移了。
参见例如this arithmetic operation conversion reference 获取更多信息。
您应该将变量(或值)转换为无符号整数(即在您的情况下为 uint16_t
):
val = static_cast<uint16_t>(val) >> 4;
如果不知道类型,比如它是一个模板参数,那么你可以使用 std::make_unsigned
:
val = static_cast<typename std::make_unsigned<T>::type>(val) >> 4;
如前所述,当您的类型为 signed
时,>>
运算符正在执行算术移位。因此,除了上面建议的解决方案之外,如果您需要进行逻辑移位,您总是可以简单地使用 mask
如下所示:
int mask = 1 << (targetSize-missingbits-1);
mask |= mask - 1;
result = (result >> missingbits) & mask;
在这种情况下,mask
将为您提供 missingbits
MSB 的 0
和其余的 1
。在您的情况下,4 个 MSB 为 0
,其余为 1
。然后,执行 &
操作将重置 result
中的第一个 missingbits
,这就是您需要的:
0xF89A & 0x0FFF = 0x089A
看到它工作 live-example。
那个带有循环的原始代码对我来说看起来有点复杂,我会这样写(我的意思是作为第二个选项,在我莫名其妙地无法避免使用 std::bitset
和模板之后完全,对于数据的位大小调整这样简单的事情,首先):
#include <bitset>
#include <climits>
template <typename T,size_t count> static T convertBitSetToNumber( const std::bitset<count>& bitset )
{
constexpr size_t targetSize = sizeof( T )*CHAR_BIT;
if (targetSize == count) return static_cast<T>(bitset.to_ullong());
if (targetSize < count) return static_cast<T>(bitset.to_ullong() >> (count - targetSize));
return static_cast<T>(bitset.to_ullong() << (targetSize - count)) >> (targetSize - count);
}
// Example test producing from 0x089A bitset unsigned/signed values:
// 16b: 89a f89a | 8b: 89 89 | 32b: 89a fffff89a
#include <iostream>
int main()
{
const std::bitset<12> testBitset("100010011010");
std::hex(std::cout);
std::cout << convertBitSetToNumber<uint16_t,12>( testBitset ) << std::endl;
std::cout << convertBitSetToNumber<int16_t,12>( testBitset ) << std::endl;
std::cout << (0xFF & convertBitSetToNumber<uint8_t,12>( testBitset )) << std::endl;
std::cout << (0xFF & convertBitSetToNumber<int8_t,12>( testBitset )) << std::endl;
std::cout << convertBitSetToNumber<uint32_t,12>( testBitset ) << std::endl;
std::cout << convertBitSetToNumber<int32_t,12>( testBitset ) << std::endl;
}