如何将8个布尔值转换为一个字节(以及反过来)?

30

我有8个bool变量,想要将它们"合并"成一个字节。

有没有简单/首选的方法可以做到这一点?

另一种方式如何,将一个字节解码为8个单独的布尔值?

我认为这不是一个不合理的问题,但由于我在Google上找不到相关的文档,可能是另一种那种"不行,你所有的直觉都是错的"情况。


我不确定你的意思。在C++中,布尔(boolean)数据类型是一个字节,你想如何将一个字节转换成字节? - ScarletAmaranth
1
无法将8个布尔变量打包成一个字节。但是可以使用位掩码将8个逻辑真/假状态打包在单个字节中。详见:http://en.wikipedia.org/wiki/Bitwise_operation - ScarletAmaranth
@ScarletAmaranth 那应该就是一个答案。 - weltraumpirat
1
@weltraumpirat 我不确定问题具体是什么。 - ScarletAmaranth
3
我知道有些人会把这个问题搞得比实际上更难,这是我的错,因为我不知道布尔值的大小超过1个比特。 - xcel
相关:如何使用x86 SIMD高效地将8位位图转换为0/1整数数组,一次可以处理16个字节。 - Peter Cordes
9个回答

27

较为困难的方法:

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++ 中是未定义行为。使用 memcpyC++20 std::bit_cast 是在可移植的 C++ 中执行类型转换的安全方式。
(此外,在 char 中的位域的位顺序是实现定义的,位域成员之间可能存在填充。)

1
@Juicy 不直接使用 union,如果需要循环,则使用 <<。但是你可以混合两种解决方案。 - rodrigo
2
@ibug 我认为在 C++ 中通过 union 进行类型游戏是未定义的行为,regehr 在这里似乎是这样说的:https://blog.regehr.org/archives/959 我不认为 trap-representations 的问题是相关的。这是关于严格别名规则的问题。上面展示的“酷”方式至少会违反我们公司的编码标准。 - Chris Beck
1
@iBug 访问一个联合体的成员,而该成员不是最后一次分配的,这是UB。 - UKMonkey
7
通过联合体进行类型转换是未定义行为;请删除此部分代码或明确说明这是一个扩展特性,并列出支持该特性的编译器。 - Baum mit Augen
3
这明显是未定义行为。 - YSC
显示剩余6条评论

16

酷炫的方法(使用乘法技巧

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,例如在二进制中打印数字时。
您可能希望确保bool数组具有8字节对齐(alignas(8))以获得更好的性能,并且编译器知道这一点。memcpy始终对任何对齐方式都安全,但在需要对齐的ISAs上,仅当编译器知道指针足够对齐时,才能将memcpy内联为单个加载或存储指令。 *(uint64_t *)a 会承诺对齐,但也违反了严格别名规则。即使在允许不对齐加载的ISAs上,当自然对齐时它们可能更快。但编译器仍然可以在编译时内联memcpy而不看到该保证。

工作原理

假设我们有8个布尔值 b[0]b[7],它们的最低有效位分别命名为 a-h,我们想将它们打包成一个单字节。将这8个连续的 bool 视为一个64位字,并加载它们,我们将在小端机器上以相反的顺序得到位。现在我们将进行乘法运算(这里点表示零位)

  |  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个最低有效位已放置在顶部字节中,我们只需要屏蔽其余位即可。
因此,打包的魔术数字将为0b10000000010000000010000000010000000010000000010000000010000000010x8040201008040201。如果您使用的是大端机器,则需要使用类似方式计算出的魔术数字0x0102040810204080
对于解包,我们可以进行类似的乘法运算。
  |  b7  ||  b6  ||  b4  ||  b4  ||  b3  ||  b2  ||  b1  ||  b0  |
                                                          abcdefgh
× 1000000001000000001000000001000000001000000001000000001000000001
  ────────────────────────────────────────────────────────────────
= h0abcdefgh0abcdefgh0abcdefgh0abcdefgh0abcdefgh0abcdefgh0abcdefgh
& 1000000010000000100000001000000010000000100000001000000010000000
  ────────────────────────────────────────────────────────────────
= h0000000g0000000f0000000e0000000d0000000c0000000b0000000a0000000

在乘法之后,我们需要将所需的位数放置在最高有效位上,因此我们需要屏蔽不相关的位并将其余位移动到最低有效位。输出将是包含a到h的字节的小端格式。


高效的方式

在具备BMI2的新型x86 CPU上,有PEXTPDEP指令可用于此目的。上面的pack8bools函数可以被替换为

_pext_u64(*((uint64_t*)a), 0x0101010101010101ULL);

unpack8bools 函数可以实现为

_pdep_u64(b, 0x0101010101010101ULL);

这将映射LSB -> LSB,就像0x0102040810204080ULL乘数常量一样,相反的是0x8040201008040201ULL。 x86是小端:memcpy后,a [0] =(b>> 0)&1;

不幸的是在Zen 3之前,这些指令在AMD上非常慢,因此您可能需要与上面的乘法方法进行比较,以确定哪种方法更好。

另一种快速方法是SSE2

x86 SIMD具有一个操作,可以获取向量寄存器中每个字节(或浮点数或双精度浮点数)的高位,并将其作为整数提供给您。 字节的指令是pmovmskb。 如果您有大量需要处理的数据,则可以使用相同数量的指令处理16个字节,因此它比乘法技巧更好。

#include <immintrin.h>

inline uint8_t pack8bools_SSE2(const bool* a)
{
    __m128i v = _mm_loadl_epi64( (const __m128i*)a );  // 8-byte load, despite the pointer type.
    // __m128 v = _mm_cvtsi64_si128( uint64 );  // alternative if you already have an 8-byte integer
    v = _mm_slli_epi32(v, 7);   // low bit of each byte becomes the highest
    return _mm_movemask_epi8(v);
}

直到AVX-512出现掩码向量指令之前,没有单个指令可以解包。使用SIMD是可行的,但可能不如乘法技巧高效。请参见将16位掩码转换为16字节掩码和更普遍地是否有反向指令以在Intel AVX2中对movemask指令进行解包?以将位图解包为其他元素大小。 如何使用x86 SIMD将8位位图有效地转换为0/1整数数组有一些针对8位 -> 8字节的具体答案,但如果您不能一次处理16位,则乘法技巧可能更好,而pext肯定是(除了在像Zen 3之前的AMD这样灾难性缓慢的CPU上)。

1
to_ullongunsigned long long 的构造函数无法使用 PEXT 和 PDEP,因为它们只是直接复制 unsigned long long 值到/从内部数组。bitset 中的位被存储为数组中的位而不是每个字节的一位。所以 to_ullong 提供了 64 位而不是 8 位。 - phuclv
1
@SamuelLi 这可能是由于严格别名规则导致的。尝试使用-fno-strict-aliasing或使用联合体。或者只需将bool更改为char,因为char*可以别名其他类型。 - phuclv
1
@SamuelLi:这是因为使用 uint64_t * 读取 char [] 对象会违反严格别名规则。 ISO C 只允许使用 char* 读取最初是 uint64_t 的对象。 如果对 char 对象的唯一访问是通过 char *,那么就可以,但如果有任何自动或静态存储 char[] 的使用,则会产生 UB。 此答案应该使用 memcpy 或 C++20 的 std::bitcast,或者 GNU C 的 typedef uint64_t unaligned_aliasing_u64 __attribute__((aligned(1), may_alias)); 来进行类型装换;指针转换永远不安全。 - Peter Cordes
1
@SamuelLi:在C++中,自动存储=非静态局部变量;静态存储=全局和静态任何东西。我的观点是,如果有某种方法可以访问数组对象本身,特别是作为结构的一部分,那么严格别名可能会出现真正的问题。如果“数组”只是您从newmalloc获取的一些内存的处理方式,那么对它的所有char访问都将通过char*进行,因此是别名安全的。但是,如果您在某个地方声明了一个真正的char array[8]变量,则对它的访问可能不是通过char*进行的。 - Peter Cordes
1
@SamuelLi:虽然我刚意识到即使使用动态存储,你仍可能会遇到问题。如果你有struct foo{ int a; char b[8];};,那么你可以通过指向它的指针进行整个结构体的结构赋值,例如foo *p=..., *q=...;*p = *q。在这种情况下,对char b[8]的访问并不是通过char*进行的,因此编译器肯定会假设某些uint64_t*的加载或存储与其无关。(实际上,你可能需要这个结构体才能使UB真正发生,或者至少在实践中成为一个问题,而不仅仅是一个char arr[8]局部变量:[]访问通过衰减到char*来完成) - Peter Cordes
显示剩余16条评论

12

你可能想要了解一下std::bitset。它允许你将布尔值紧凑地存储为位,具有你所期望的所有运算符。

当你可以抽象出来时,就没有必要在位翻转等方面胡闹了。


1
请参阅cppreference - kebs

6
#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);
}

9
发布一个没有任何解释的代码解决方案可能会帮助提问者,但对其他用户没有太大的价值。你应该考虑添加注释和/或解释你所做的事情。 - weltraumpirat
在使用时,如果需要恰好8位,则应使用uint8_t类型,这正是该类型的用途。 - user406009
1
希望你意识到 eightBools[i] 是一个 bool 类型,检查它时使用 == true 也可以直接写成 (eightBools[i] == true) == true 或者 ((eightBools[i] == true) == true) == true,但是这种方式何时停止呢?是的,这并不值得点赞。 - Christian Rau
weltraumpirat 我认为代码已经足够简单明了,不需要额外的解释反而会妨碍理解。Christian,我接受你的批评 -- 在我的代码中我不会这样写 -- 但是在这里我这样写是为了更清晰地表达代码的意图。你可以保留你的赞,我不需要它 ;) - Jeremy Friesner
@JeremyFriesner 既然你说这不是你的习惯,那你可以随意处理。对于我之前的严厉措辞感到抱歉 ;) - Christian Rau
4
对于一个新手来说,你的代码使用位掩码并不直观,一点也不(双关语)。我仍然坚持我之前的评论。 - weltraumpirat

2

2
这不是太过硬编码了吗? - Fabián Heredia Montiel
2
取决于情况,我不会这样写,但如果你想合并8个不连续的布尔值,那么这就是正确的方法。 - 111111

2

我想指出,在C++中,通过union进行类型游戏是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实现类型转换的方式。


@Michi 这个问题是关于C++的吗? - iBug
1
虽然我喜欢这个解决方案的简单性,但是它看起来似乎位域的内存布局是依赖于编译器的链接。这对于某些用例来说是不期望的... - Samuel Li

1

无法将8个 bool 变量压缩成一个字节。 但是可以使用 位掩码 将8个逻辑 true/false 状态打包到单个字节中。


你可以将8个布尔值的打包成一个字节。当然,它们不再是单独的C++ bool对象,但所有8个值都在一个charuint8_t的位中。当期望的行为很明确时,这个答案似乎没有帮助性和迂腐。 - Peter Cordes
我无法读取思维以确定意图,因为在回答时我的水晶球已经坏了。我回答了提出的问题。 - ScarletAmaranth

0
你可以使用位移操作和类型转换来实现它。一个函数可能会像这样工作:
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的纠正s


1
我(过去式)不懂任何C++...所以如果有人能够解释为什么这个问题被踩,将不胜感激! - user166390
1
那些给我点踩的人能否告诉我我做错了什么?我对编程很幼稚。 :/ - Fabián Heredia Montiel
2
你使用 short (可能是 1 个字节,但很可能是 2 个字节) 而不是 char (保证为 1 个字节) 的原因是什么?并且你应该使用无符号类型并正确初始化 byte。修复这些问题,答案更有可能是正确的。但我不是投票反对者,至少还不是。 - Christian Rau
当然,它肯定可以在C和C++上运行。毕竟C++是C的扩展。 - Fabián Heredia Montiel
1
unsigned char byte = \0; is invalid. Either use 0 or '\0' - phuclv
显示剩余5条评论

0
如果你想要更少的硬编码,将8个拆分布尔值,也许...
bool a,b,c,d,e,f,g,h;      # assuming you want [a] at MSB and 
                           #                   [h] at LSB

char y = a << 7 | b<< 6 | c << 5 | d << 4 | e << 3 | f << 2 | g << 1 | h
char y = h|(g|(f|(e|(d|(c|(b|a << 1)<< 1)<< 1)<< 1)<< 1)<< 1)<< 1

...而不是使用7个不同的移位量,嵌套形式允许反复使用相同的移位常数,

同时还有一个额外的好处,即将所有的布尔值/数字放在一边,将所有的移位量放在另一边

算术等价表达式如下(它们都是相同的)-

char y = h + (g + (f + (e + (d + (c + (b + a * 2) * 2) * 2) * 2) * 2) * 2) * 2

char y = 2 * (2 * (2 * (2 * (2 * (2 * (2 * a + b) + c) + d) + e) + f) + g) + h

char y = 2 * ( 2 * ( 2 * ( 2 * ( 2 * ( 2 * ( 2 * (\
            a ) + b ) + c ) + d ) + e ) + f ) + g ) + h

个人而言,我最喜欢最后一种形式,因为它是一个完整的进制转换程序,而不需要观众甚至理解什么是进制和指数,或者什么是移位,只要他们理解加法和乘法即可。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接