从位域中获取全部价值

Getting entire value from bitfields

我想创建一个块结构用于我正在构建的体素游戏(只是背景上下文),但是我在保存和加载方面遇到了 运行 问题。

我可以将一个块表示为单个 Uint16 并移动位以获得不同的元素,例如块 ID 和健康,或者我可以使用如下所示的位域:

struct Block
{
    Uint16 id : 8;
    Uint16 health : 6;
    Uint16 visible : 1;
    Uint16 structural : 1;
}

使用第一种方法,当我想保存Block数据时,我可以简单地将Uint16的值转换成十六进制值并将其写入文件。通过加载,我可以简单地读取数字并将其转换回来,然后通过手动位移返回读取各个位。

我的问题是我不知道如何通过位域方法获取我使用的 Uint16 的全部值,这意味着我无法将块数据保存为单个十六进制值。

所以,问题是如何将实际的单个 Uint16 存储在由不同位字段组成的块结构中。如果不可能,那也没关系,因为我已经说过,我的手动位移方法工作得很好。我只是想分析一下哪种存储和修改数据的方法真的更快。

如果我遗漏了一个关键细节,或者您需要任何额外的信息来帮助我,一定要问。

这不是位域的良好用法(实际上,很少)。

不能保证您的位字段的顺序与它们声明的顺序相同;它可能会在您的应用程序构建之间发生变化。

您必须使用移位和按位或运算符将您的成员手动存储在 uint16_t 中。作为一般规则,在处理外部存储时,永远不要只是转储或盲目复制数据;您应该手动 serialize/deserialize 它,以确保它是您期望的格式。

至少有两种方法可以满足您的需求:

  • 位移位
  • 铸造

位移位

您可以通过将位字段移动到 uint16_t 来从您的结构构建 uint16_t

uint16_t halfword;
struct Bit_Fields my_struct;
halfword = my_struct.id << 8;
halfword = halfword | (my_struct.health << 2);
halfword = halfword | (my_struct.visible << 1);
halfword = halfword | (my_struct.structural);

选角

另一种方法是将结构实例转换为uint16_t

uint16_t halfword;
struct Bit_Fields my_struct;
halfword = (uint16_t) my_struct;

字节顺序

一个值得关注的问题是字节顺序;或多字节值的字节顺序。这可能与位在 16 位单元中的位置有关。

您可以使用联合:

typedef union
{
    struct
    {
        Uint16 id : 8;
        Uint16 health : 6;
        Uint16 visible : 1;
        Uint16 structural : 1;
    } Bits;

    Uint16 Val;
} TMyStruct;

联合可能是最干净的方式:

#include <iostream>

typedef unsigned short Uint16;

struct S {
  Uint16 id : 8;
  Uint16 health : 6;
  Uint16 visible : 1;
  Uint16 structural : 1;
};
union U {
 Uint16 asInt;
 S asStruct;
};

int main() {
  U u;
  u.asStruct.id = 0xAB;
  u.asStruct.health = 0xF;
  u.asStruct.visible = 1;
  u.asStruct.structural = 1;
  std::cout << std::hex << u.asInt << std::endl;
}

这会打印出 cfab

更新:

在进一步考虑和更深入地阅读之后,我认为任何类型的双关语都是不好的。相反,我建议您咬紧牙关,明确地进行一些位操作来构建您的序列化值:

#include <iostream>

typedef unsigned short Uint16;

struct Block
{
  Uint16 id : 8;
  Uint16 health : 6;
  Uint16 visible : 1;
  Uint16 structural : 1;

  operator Uint16() {
    return structural | visible << 2 | health << 4 | id << 8;
  }
};

int main() {
  Block b{0xAB, 0xF, 1, 1};
  std::cout << std::hex << Uint16(b) << std::endl;
}

这有进一步的好处,它打印 abf5 匹配初始化顺序。

如果您担心性能,可以使用编译器优化掉的函数而不是 operator 成员函数:

...

constexpr Uint16 serialize(const Block& b) {
  return b.structural | b.visible << 2 | b.health << 4 | b.id << 8;
}

int main() {
  Block b{0xAB, 0xF, 1, 1};
  std::cout << std::hex << serialize(b) << std::endl;
}

最后,如果速度比内存更重要,我建议去掉位域:

struct Block
{
  Uint16 id;
  Uint16 health;
  Uint16 visible;
  Uint16 structural;
};

生活在边缘(未定义行为)..

天真的解决方案是 reinterpret_cast 将对象引用到位域的基础类型,滥用第一个非静态的事实标准布局 class 的数据成员与对象本身位于同一地址。

struct A {         
  uint16_t         id : 8;
  uint16_t     health : 6;
  uint16_t    visible : 1;
  uint16_t structural : 1;
};

A a { 0, 0, 0, 1 };
uint16_t x = reinterpret_cast<uint16_t const&> (a);

上面的内容可能看起来很准确,而且 经常(不总是)产生预期的结果 - 但它有两个大问题:

  • 对象中位域的分配是实现定义的,并且;
  • class类型必须是标准布局


没有人说 位字段 会在物理上按照您声明它们的顺序存储,即使是这种情况,编译器也可能会在每个位字段之间插入填充(因为这是允许的)。

总结一下; 位域如何在内存中结束是高度实现定义的,试图推断行为需要你查看你的实现文档重要的。


使用 union 怎么样?

  • Accessing inactive union member - undefined?

推荐

坚持使用 bit-fiddling 方法,除非你能绝对证明 运行 代码所在的每个实现都按照你希望的方式处理它.


标准 (N4296) 是怎么说的?

9.6p1 Bit-fields [class.bit]

[...] Allocation of bit-fields within a class object is implementation-defined. Alignment of bit-fields is implementation-defined. [...]

9.2p20 Classes [class]

If a standard-layout class object has any non-static data members, its address is the same as the address of its first non-static data member. [...]