与位字段的并集为位字段成员提供了意外值

Union with bitfield gives unexpected value to bitfield members

本文关键字:字段 意外 成员      更新时间:2023-10-16

我有以下构造,旨在获取一个包含四个12位值的48位值并提取它们。

struct foo {
union {
unsigned __int64 data;
struct {
unsigned int a      : 12;
unsigned int b      : 12;
unsigned int c      : 12;
unsigned int d      : 12;
unsigned int unused : 16;
};
};
} foo;

然后使用分配有问题的数据

foo.data = (unsigned __int64)Value;

Value这里最初是用于存储数据的double。

我在做比特字段时的假设是

  • 数据保存变量应该足够大,可以保存整个数据和无符号。因此,一个无符号__int64可容纳48位
  • 每个位字段成员的类型应足够大,以容纳分配给它的且无符号的位数
  • 如果可能的(以避免对齐问题?)
  • 实际上并不需要未使用的成员

这些正确吗?

测试

Value = 206225551364

我们得到一个,它应该包含位

0000 0011 0000 0000 0100 0000 0000 0011 0000 0000 0100‬

这将导致

a: 0000 0000 0100‬ = 4
b: 0000 0000 0011 = 3
c: 0000 0000 0100 = 4
d: 0000 0000 0011 = 3

但运行此操作时,实际返回的值是

a: 4
b: 3
c: 48
d: 0

尽管这些值应该适合unsigned int的,但在使用的类型之间切换有时会更改值。因此,感觉这与数据添加到位字段时的解释方式有关。

通过添加#pragma pack(1),我知道这与对齐有关,但并不经常遇到,我突然得到了预期的值。

struct foo {
union {
unsigned __int64 data;
#pragma pack(1)
struct {
unsigned int a      : 12;
unsigned int b      : 12;
unsigned int c      : 12;
unsigned int d      : 12;
unsigned int unused : 16;
};
};
} foo;
a: 4
b: 3
c: 4
d: 3

但我觉得接受这一点并不舒服。我想了解它,从而确保它确实有效,而不仅仅是在值不超过4位的情况下看起来有效。

所以,

  • 为什么我一开始就看到这个问题
  • #pragma pack语句是如何修复该问题的
  • 人们能推断出这何时会成为一个问题吗?是因为值是12位,而不是8位或16位

为什么我一开始就看到这个问题?

首先,访问非活动的联合成员具有未定义的行为。但让我们假设您的系统允许它。

CCD_ 1可能是32比特。a和b适合于总共占用24个比特的第一CCD_ 2。这个CCD_ 3只剩下8个比特。12位的c不适合这个8位的槽。因此,它转而启动一个新的unsigned,留下8位填充。

这是一个可能的结果。位字段布局由实现定义。在另一个系统上,你可能会看到你所期望的结果。或者输出与您期望的不同,也与您在这里观察到的不同。

#pragma-pack语句是如何修复问题的?

它可能会更改布局规则;跨骑;跨多个底层对象的位字段的大小。这可能会使访问它的速度慢一点。

人们能推断出这何时会成为一个问题吗?

如果您不尝试跨越底层对象,那么布局是否支持这一点就没有区别。在这种情况下,您可以简单地使用一个64位的底层对象。

不过,这并不是比特字段布局可能与预期不同的唯一方式。例如,位字段可以是最重要的第一位或最后一位。unsigned本身中的位数是实现定义的。

一般来说,不应该依赖位集的布局。

我想达到的最佳效果将如何实现?

为了避免UB,您可以创建另一个对象,并将字节从一个复制到另一个,而不是通过并集进行punning。但首先,您必须确保对象具有相同的大小。复制可以用std::memcpystd::bit_cast来完成。

为了避免跨区问题,请使用完全填充每个底层对象的位字段集。在这种情况下,使用64位底层对象。

为了获得可靠的布局,首先不要使用位字段。bartop展示了如何通过轮班和戴口罩来做到这一点。(尽管布局仍然依赖于字节序)

无论您在做什么,通过union的长话短说数据都是未定义的行为。因此,它起作用,而不仅仅是偶然。使用union,您唯一可以做的事情就是阅读您上次写信给的成员。你做了其他任何事情,你的程序无效。

编辑:

即使这是允许的,如果没有unsigned0,也依赖于结构中的数据对齐。可能是32或64位。所以在这种情况下,你的结构在内存中看起来真的是这样的:

struct {
unsigned int a      : 12;
unsigned int a_align: 20;
unsigned int b      : 12;
unsigned int b_align: 20;
unsigned int c      : 12;
unsigned int c_align: 20;
unsigned int d      : 12;
unsigned int d_align: 20;
unsigned int unused : 16;
unsigned int unused_align: 16;
};

如果你想从结构中提取一些数据,你可能应该使用这样的掩码和位移:

unsigned mask12 = 0xFFF;//1 on first 12 least significant bits
unsigned a = data & mask12;
unsigned b = (data >> 12) & mask12;
unsigned c = (data >> 24) & mask12;
unsigned d = (data >> 36) & mask12;