与位域联合为位域成员提供意想不到的价值

Union with bitfield gives unexpected value to bitfield members

我有以下构造,用于获取包含四个 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用来存储数据

我在制作位域时的假设是

这些正确吗?

测试

Value = 206225551364

我们得到一个 Value 应该包含位

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 位时。

所以,

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

编辑:

即使这是允许的,如果没有 #pragma pack,您将依赖于结构内的数据对齐。这可能是 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;

why am I seeing the issue to begin with?

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

unsigned 大概是 32 位。 a 和 b 适合第一个 unsigned 总共占用 24 位。这个unsigned只剩下8位了。 12 位 c 不适合这个 8 位插槽。因此,它开始一个新的 unsigned 并留下 8 位填充。

这是一种可能的结果。位域布局是实现定义的。在另一个系统上,您可能会看到预期的结果。或者输出与您期望的和您在此处观察到的不同。

What does the #pragma pack statement do that fixes the issue?

它可能更改了布局规则以允许“跨越”多个底层对象的位域。这可能会使访问它 有点 慢。

Can one deduce when this will become a problem and not?

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

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

总的来说,bitsets 的布局是不应该依赖的。

How would, what I want to achieve best be done then?

为了避免 UB,您可以创建另一个对象,然后将字节从一个复制到另一个,而不是通过联合进行双关。但首先,您必须确保对象具有相同的大小。可以使用 std::memcpystd::bit_cast.

进行复制

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

要获得可靠的布局,首先不要使用位域bartop 展示了如何使用班次和面具来做到这一点。 (虽然,布局仍然依赖字节序)