我有 8bool
个变量,我想将它们“合并”成一个字节。
有没有简单/首选的方法来做到这一点?
反过来,将一个字节解码为 8 个独立的布尔值怎么样?
我进来假设这不是一个不合理的问题,但由于我无法通过 Google 找到相关文档,这可能是另一种“你的直觉都是错误的”案例。
我有 8bool
个变量,我想将它们“合并”成一个字节。
有没有简单/首选的方法来做到这一点?
反过来,将一个字节解码为 8 个独立的布尔值怎么样?
我进来假设这不是一个不合理的问题,但由于我无法通过 Google 找到相关文档,这可能是另一种“你的直觉都是错误的”案例。
艰难的道路:
unsigned char ToByte(bool b[8])
{
unsigned char c = 0;
for (int i=0; i < 8; ++i)
if (b[i])
c |= 1 << i;
return c;
}
和:
void FromByte(unsigned char c, bool b[8])
{
for (int i=0; i < 8; ++i)
b[i] = (c & (1<<i)) != 0;
}
或者很酷的方式:
struct Bits
{
unsigned b0:1, b1:1, b2:1, b3:1, b4:1, b5:1, b6:1, b7:1;
};
union CBits
{
Bits bits;
unsigned char byte;
};
然后您可以分配给工会的一个成员并从另一个成员那里读取。但请注意,位的顺序Bits
是实现定义的。
请注意,在编写另一个联合成员之后读取一个联合成员在 ISO C99 中是明确定义的,并且作为几个主要 C++ 实现(包括 MSVC 和 GNU 兼容的 C++ 编译器)的扩展,但在 ISO C++ 中是未定义的行为。 memcpy
或C++20std::bit_cast
是在可移植 C++ 中键入双关语的安全方法。
(此外,a 中位域的位顺序char
是实现定义的,位域成员之间可能有填充。)
你可能想调查一下std::bitset
。它允许您使用您期望的所有运算符将布尔值紧凑地存储为位。
当你可以抽象出来时,没有必要用比特翻转和诸如此类的东西鬼混。
inline uint8_t pack8bools(bool* a)
{
uint64_t t;
memcpy(&t, a, sizeof t); // strict-aliasing & alignment safe load
return 0x8040201008040201ULL*t >> 56;
// bit order: a[0]<<7 | a[1]<<6 | ... | a[7]<<0 on little-endian
// for a[0] => LSB, use 0x0102040810204080ULL on little-endian
}
void unpack8bools(uint8_t b, bool* a)
{
// on little-endian, a[0] = (b>>7) & 1 like printing order
auto MAGIC = 0x8040201008040201ULL; // for opposite order, byte-reverse this
auto MASK = 0x8080808080808080ULL;
uint64_t t = ((MAGIC*b) & MASK) >> 7;
memcpy(a, &t, sizeof t); // store 8 bytes without UB
}
假设sizeof(bool) == 1
要便携地执行 LSB <-> a[0]
(如pext/pdep
下面的版本)而不是使用相反的主机字节序,htole64(0x0102040810204080ULL)
请在两个版本中用作魔术乘数。(htole64
来自 BSD / GNU <endian.h>)。这会安排乘数字节以匹配 bool 数组的小端顺序。 htobe64
使用相同的常数给出另一个顺序,MSB-first 就像您用于打印以 2 为底的数字一样。
您可能需要确保 bool 数组是 8 字节对齐 ( alignas(8)
) 以提高性能,并且编译器知道这一点。 memcpy
对于任何对齐总是安全的,但是在需要对齐的 ISA 上,如果编译器知道指针已充分对齐,则它只能memcpy
作为单个加载或存储指令内联。 *(uint64_t*)a
会承诺对齐,但也违反严格混叠规则。即使在允许未对齐负载的 ISA 上,自然对齐时它们也可以更快。但是编译器仍然可以内联 memcpy 而不会在编译时看到该保证。
假设我们有 8 个布尔值b[0]
,它们b[7]
的最低有效位分别命名为 ah,我们希望将它们打包成一个字节。将这 8 个连续bool
的 s 视为一个 64 位字并加载它们,我们将在 little-endian 机器中以相反的顺序获得这些位。现在我们将做一个乘法(这里的点是零位)
| b7 || b6 || b4 || b4 || b3 || b2 || b1 || b0 |
.......h.......g.......f.......e.......d.......c.......b.......a
× 1000000001000000001000000001000000001000000001000000001000000001
────────────────────────────────────────────────────────────────
↑......h.↑.....g..↑....f...↑...e....↑..d.....↑.c......↑b.......a
↑.....g..↑....f...↑...e....↑..d.....↑.c......↑b.......a
↑....f...↑...e....↑..d.....↑.c......↑b.......a
+ ↑...e....↑..d.....↑.c......↑b.......a
↑..d.....↑.c......↑b.......a
↑.c......↑b.......a
↑b.......a
a
────────────────────────────────────────────────────────────────
= abcdefghxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
添加了箭头,因此更容易看到设置位在幻数中的位置。此时已将 8 个最低有效位放入最高字节,我们只需将其余位屏蔽掉
所以包装的神奇数字是0b1000000001000000001000000001000000001000000001000000001000000001
or 0x8040201008040201
。如果您使用的是大端机器,则需要使用0x0102040810204080
以类似方式计算的幻数
对于解包,我们可以做一个类似的乘法
| b7 || b6 || b4 || b4 || b3 || b2 || b1 || b0 |
abcdefgh
× 1000000001000000001000000001000000001000000001000000001000000001
────────────────────────────────────────────────────────────────
= h0abcdefgh0abcdefgh0abcdefgh0abcdefgh0abcdefgh0abcdefgh0abcdefgh
& 1000000010000000100000001000000010000000100000001000000010000000
────────────────────────────────────────────────────────────────
= h0000000g0000000f0000000e0000000d0000000c0000000b0000000a0000000
相乘之后,我们在最高有效位置得到了所需的位,因此我们需要屏蔽掉不相关的位并将剩余的位移到最低有效位置。输出将是包含 a 到 h 的小端字节数。
在具有BMI2的较新的 x86 CPU 上,有用于此目的的PEXT和PDEP指令。上面的pack8bools
函数可以替换为
_pext_u64(*((uint64_t*)a), 0x0101010101010101ULL);
该unpack8bools
功能可以实现为
_pdep_u64(b, 0x0101010101010101ULL);
(这映射 LSB -> LSB,就像一个0x0102040810204080ULL
乘数常数,与 .x86 相对的0x8040201008040201ULL
是 little-endian:a[0] = (b>>0) & 1;
在 memcpy 之后。)
不幸的是,这些指令在 AMD 上非常慢,因此您可能需要与上面的乘法方法进行比较,看看哪个更好
#include <stdint.h> // to get the uint8_t type
uint8_t GetByteFromBools(const bool eightBools[8])
{
uint8_t ret = 0;
for (int i=0; i<8; i++) if (eightBools[i] == true) ret |= (1<<i);
return ret;
}
void DecodeByteIntoEightBools(uint8_t theByte, bool eightBools[8])
{
for (int i=0; i<8; i++) eightBools[i] = ((theByte & (1<<i)) != 0);
}
bool a,b,c,d,e,f,g,h;
//do stuff
char y= a<<7 | b<<6 | c<<5 | d<<4 | e <<3 | f<<2 | g<<1 | h;//merge
尽管您可能最好使用 bitset
我想指出,通过union
s 进行双关的类型是 C++ 中的 UB(正如rodrigo在他的回答中所做的那样。最安全的方法是memcpy()
struct Bits
{
unsigned b0:1, b1:1, b2:1, b3:1, b4:1, b5:1, b6:1, b7:1;
};
unsigned char toByte(Bits b){
unsigned char ret;
memcpy(&ret, &b, 1);
return ret;
}
正如其他人所说,编译器足够聪明,可以优化memcpy()
.
顺便说一句,这就是 Boost 键入双关语的方式。
您将使用按位移位操作和强制转换来存档它。一个函数可以像这样工作:
unsigned char toByte(bool *bools)
{
unsigned char byte = \0;
for(int i = 0; i < 8; ++i) byte |= ((unsigned char) bools[i]) << i;
return byte;
}
感谢Christian Rau的更正!
没有办法将 8bool
个变量打包成一个字节。有一种方法使用Bitmasking将 8 个逻辑真/假状态打包在一个字节中。