从位域中获取全部价值
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. [...]
我想创建一个块结构用于我正在构建的体素游戏(只是背景上下文),但是我在保存和加载方面遇到了 运行 问题。
我可以将一个块表示为单个 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. [...]