repr(Rust)

首先,所有类型都有一个以字节为单位指定的对齐方式。类型的对齐方式指定了哪些地址可以存储该值。对齐方式为 `n` 的值只能存储在 `n` 的倍数的地址处。因此,对齐方式为 2 意味着必须将其存储在偶数地址处,而 1 意味着可以将其存储在任何位置。对齐方式至少为 1,并且始终为 2 的幂。

基本类型通常与其大小对齐,但这取决于平台。例如,在 x86 上,`u64` 和 `f64` 通常与 4 字节(32 位)对齐。

类型的大小必须始终是其对齐方式的倍数(零是任何对齐方式的有效大小)。这确保了该类型的数组始终可以通过偏移其大小的倍数来索引。请注意,对于动态大小类型,类型的大小和对齐方式可能无法静态得知。

Rust 提供以下几种方式来布局组合数据:

  • 结构体(命名积类型)
  • 元组(匿名积类型)
  • 数组(同构积类型)
  • 枚举(命名和类型 - 带标签的联合)
  • 联合(不带标签的联合)

如果枚举的任何变体都没有关联数据,则称该枚举为*无字段*枚举。

默认情况下,组合结构的对齐方式等于其字段对齐方式的最大值。因此,Rust 将在必要的位置插入填充,以确保所有字段都正确对齐,并且整体类型的大小是其对齐方式的倍数。例如:

#![allow(unused)]
fn main() {
struct A {
    a: u8,
    b: u32,
    c: u16,
}
}

在将这些基本类型与其各自大小对齐的目标上,将进行 32 位对齐。因此,整个结构体的大小将是 32 位的倍数。它可能会变成:

#![allow(unused)]
fn main() {
struct A {
    a: u8,
    _pad1: [u8; 3], // to align `b`
    b: u32,
    c: u16,
    _pad2: [u8; 2], // to make overall size multiple of 4
}
}

或者:

#![allow(unused)]
fn main() {
struct A {
    b: u32,
    c: u16,
    a: u8,
    _pad: u8,
}
}

这些类型*没有间接寻址*;所有数据都存储在结构体中,就像在 C 语言中所预期的那样。但是,除了数组(它们是紧密排列且有序的)之外,数据的布局默认情况下没有指定。给定以下两个结构体定义:

#![allow(unused)]
fn main() {
struct A {
    a: i32,
    b: u64,
}

struct B {
    a: i32,
    b: u64,
}
}

Rust *确实*保证了 A 的两个实例的数据布局完全相同。但是,Rust *目前不*保证 A 的实例与 B 的实例具有相同的字段顺序或填充。

根据 A 和 B 的编写方式,这一点似乎有些吹毛求疵,但是 Rust 的其他一些特性使得该语言需要以复杂的方式处理数据布局。

例如,考虑以下结构体:

#![allow(unused)]
fn main() {
struct Foo<T, U> {
    count: u16,
    data1: T,
    data2: U,
}
}

现在考虑 `Foo<u32, u16>` 和 `Foo<u16, u32>` 的单态化。如果 Rust 按指定的顺序排列字段,我们希望它填充结构体中的值以满足其对齐要求。因此,如果 Rust 不对字段重新排序,我们希望它生成以下内容:

struct Foo<u16, u32> {
    count: u16,
    data1: u16,
    data2: u32,
}

struct Foo<u32, u16> {
    count: u16,
    _pad1: u16,
    data1: u32,
    data2: u16,
    _pad2: u16,
}

后一种情况显然是在浪费空间。优化空间使用需要不同的单态化具有*不同的字段顺序*。

枚举使这种考虑更加复杂。简单地说,像这样的枚举:

#![allow(unused)]
fn main() {
enum Foo {
    A(u32),
    B(u64),
    C(u8),
}
}

可能会被布局为:

#![allow(unused)]
fn main() {
struct FooRepr {
    data: u64, // this is either a u64, u32, or u8 based on `tag`
    tag: u8,   // 0 = A, 1 = B, 2 = C
}
}

实际上,这大致就是它的布局方式(模的大小和 `tag` 的位置)。

但是,在某些情况下,这种表示方式效率低下。一个典型的例子是 Rust 的“空指针优化”:由单个外部单元变体(例如 `None`)和一个(可能嵌套的)非空指针变体(例如 `Some(&T)`) 组成的枚举不需要标签。空指针可以安全地解释为单元(`None`)变体。最终结果是,例如,`size_of::<Option<&T>>() == size_of::<&T>()`。

Rust 中有许多类型是或包含非空指针,例如 `Box<T>`、`Vec<T>`、`String`、`&T` 和 `&mut T`。类似地,可以想象嵌套枚举将其标签合并到一个判别式中,因为根据定义,它们已知具有有限范围的有效值。原则上,枚举可以使用相当复杂的算法将位存储在嵌套类型中,并使用禁止的值。因此,我们*特别*希望今天不要指定枚举布局。