与位域联合为位域成员提供意想不到的价值
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用来存储数据
我在制作位域时的假设是
- 数据保持变量应该足够大以容纳整个
数据和未签名。所以 unsigned __int64 可以容纳 48 位。
- 每个位域成员的类型都应该足够大以容纳
分配给它的无符号位数。
- 最好让所有位域成员保持同一类型,如果
可能的。 (以避免对齐问题?)
- unused 成员实际上并不需要。
这些正确吗?
测试
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 位时。
所以,
- 为什么我一开始就看到这个问题?
- #pragma pack 语句解决问题的作用是什么?
- 可以推断出这什么时候会成为问题,什么时候不会?是不是因为
例如,值是 12 位而不是 8 位或 16 位?
长话短说 - 通过 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::memcpy
或 std::bit_cast
.
进行复制
为避免跨接问题,请使用完全填充每个底层对象的位域集。在这种情况下,使用 64 位底层对象。
要获得可靠的布局,首先不要使用位域。 bartop 展示了如何使用班次和面具来做到这一点。 (虽然,布局仍然依赖字节序)
我有以下构造,用于获取包含四个 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用来存储数据
我在制作位域时的假设是
- 数据保持变量应该足够大以容纳整个 数据和未签名。所以 unsigned __int64 可以容纳 48 位。
- 每个位域成员的类型都应该足够大以容纳 分配给它的无符号位数。
- 最好让所有位域成员保持同一类型,如果 可能的。 (以避免对齐问题?)
- unused 成员实际上并不需要。
这些正确吗?
测试
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 位时。
所以,
- 为什么我一开始就看到这个问题?
- #pragma pack 语句解决问题的作用是什么?
- 可以推断出这什么时候会成为问题,什么时候不会?是不是因为 例如,值是 12 位而不是 8 位或 16 位?
长话短说 - 通过 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::memcpy
或 std::bit_cast
.
为避免跨接问题,请使用完全填充每个底层对象的位域集。在这种情况下,使用 64 位底层对象。
要获得可靠的布局,首先不要使用位域。 bartop 展示了如何使用班次和面具来做到这一点。 (虽然,布局仍然依赖字节序)