我最近通读了Type Layout,对枚举上原始 repr 的描述感到惊讶。请参阅带有字段的枚举的原始表示和将枚举的原始表示与字段和 组合#[repr(C)]
。我的问题是,这两种表示不是等价的吗?
更具体地说,考虑这个枚举:
enum WithFields {
A(u16),
B(u8, u16),
C(u32, u32),
}
如果用 注释repr(u8)
,它将具有与UnionWithTag
以下相同的表示:
#[repr(C)]
union UnionWithTag {
a: VariantA,
b: VariantB,
c: VariantC,
}
#[derive(Copy, Clone)]
#[repr(u8)]
pub enum Tag {
A,
B,
C,
}
#[derive(Copy, Clone)]
#[repr(C)]
struct VariantA(Tag, u16);
#[derive(Copy, Clone)]
#[repr(C)]
struct VariantB(Tag, u8, u16);
#[derive(Copy, Clone)]
#[repr(C)]
struct VariantC(Tag, u32, u32);
如果用 注释repr(C, u8)
,它将具有与TaggedUnion
以下相同的表示形式:
#[repr(C)]
struct TaggedUnion {
tag: Tag,
payload: Union,
}
#[derive(Copy, Clone)]
#[repr(u8)]
pub enum Tag {
A,
B,
C,
}
#[repr(C)]
pub union Union {
a: PayloadA,
b: PayloadB,
c: PayloadC,
}
#[derive(Copy, Clone)]
#[repr(C)]
struct PayloadA(u16);
#[derive(Copy, Clone)]
#[repr(C)]
struct PayloadB(u8, u16);
#[derive(Copy, Clone)]
#[repr(C)]
struct PayloadC(u32, u32);
TaggedUnion
和UnionWithTag
布局不一样吗?
它们可能看起来是相同的,但
repr(C, u8)
版本最终会比repr(u8)
版本有更多的填充。考虑枚举的简化版本,仅包含B
变体:如
repr(u8)
:并作为
repr(C, u8)
:我们将自下而上地计算
TaggedUnion
和的总体大小UnionWithTag
。PayloadB
需要在字段后有一个填充字节u8
以正确对齐u16
字段。VariantB
的字段全部组合在一起,没有填充(u8
标签和u8
字段完美对齐u16
字段)TaggedUnion
是一个u8
标签,后跟 (一个并集,仅包含)PayloadB
,它具有对齐 2。这会在标签后面强制填充字节,以正确对齐它。总体而言,大小为 6,对齐为 2。总体为 2 个填充字节。UnionWithTag
is (a union, contains only)VariantB
,其大小为 4,对齐为 2。不使用填充。这并不是对为什么布局不等价的真正探索,但它是它们确实不等价的有效的矛盾证明。
在这两个示例中,
VariantC
/PayloadC
的对齐方式为 4,而其余的Variant
/Payload
的对齐方式为 2。在
TaggedUnion
示例中,Union
对齐方式为 4,这是其成员的最大对齐方式。因此,TaggedUnion
在所有情况下,必须在 u8 标记和联合之间放置三个字节的填充,以满足 的最坏情况PayloadC
。然而,
Variant
s 具有更大的灵活性。VariantA
的 u16 成员只需要对齐 2,因此它只需要在标记与其数据之间填充一个字节。VariantB
可以紧密地打包其数据而无需任何填充。VariantC
仍然需要 3 个字节的填充。不过,当它们被放置在 中时,额外的填充会回来Union
,但它位于联合的末尾。因此,由于内部标记的
Variant
s 知道它们将包含标签,因此它们可以将数据移近标签,但由于Payload
s 可能作为标签外部的单独结构存在,因此它们需要自行对齐,从而使两种方法布局-不相容。