什么是Rust中的“胖指针”?

Luk*_*odt 64 pointers rust

我已经在多个上下文中阅读过“胖指针”一词,但是我不确定它的确切含义以及何时在Rust中使用它。该指针似乎是普通指针的两倍,但我不明白为什么。它也似乎与特征对象有关。

Luk*_*odt 66

术语“胖指针”用于表示对动态大小类型(DST)–切片或特征对象的引用和原始指针。胖指针包含一个指针以及一些使DST“完成”的信息(例如长度)。

Rust中最常用的类型不是 DST,而是在编译时已知的固定大小。这些类型实现Sized特质。甚至管理动态大小的堆缓冲区的类型(例如Vec<T>),Sized因为编译器都知道Vec<T>实例将在堆栈上占用的确切字节数。目前,Rust中有四种不同的DST。


切片([T]str

类型[T](对于any T)是动态调整大小的(特殊的“字符串切片”类型也是如此str)。这就是为什么您通常只将其视为&[T]&mut [T],即在引用后面。该参考是所谓的“胖指针”。让我们检查:

dbg!(size_of::<&u32>());
dbg!(size_of::<&[u32; 2]>());
dbg!(size_of::<&[u32]>());
Run Code Online (Sandbox Code Playgroud)

打印(进行一些清理):

size_of::<&u32>()      = 8
size_of::<&[u32; 2]>() = 8
size_of::<&[u32]>()    = 16
Run Code Online (Sandbox Code Playgroud)

因此,我们看到对普通类型u32的引用(如对array的引用)的长度为8个字节[u32; 2]。这两种类型不是DST。但是与[u32]DST一样,对其的引用也要大一倍。对于切片,“完成” DST的其他数据就是长度。因此,可以说的表示形式&[u32]是这样的:

struct SliceRef { 
    ptr: *const u32, 
    len: usize,
}
Run Code Online (Sandbox Code Playgroud)


特质对象(dyn Trait

当使用特征作为特征对象(即类型被擦除,动态调度)时,这些特征对象就是DST。例:

trait Animal {
    fn speak(&self);
}

struct Cat;
impl Animal for Cat {
    fn speak(&self) {
        println!("meow");
    }
}

dbg!(size_of::<&Cat>());
dbg!(size_of::<&dyn Animal>());
Run Code Online (Sandbox Code Playgroud)

打印(进行一些清理):

size_of::<&Cat>()        = 8
size_of::<&dyn Animal>() = 16
Run Code Online (Sandbox Code Playgroud)

同样,&Cat由于Cat是普通类型,因此只有8个字节大。但是dyn Animal是特征对象,因此具有动态大小。因此,&dyn Animal为16个字节。

对于特征对象,完成DST的其他数据是指向vtable(vptr)的指针。我在这里不能完全解释vtables和vptrs的概念,但是它们用于在此虚拟调度上下文中调用正确的方法实现。vtable是静态数据,基本上每个方法只包含一个函数指针。这样,对特征对象的引用基本上表示为:

struct TraitObjectRef {
    data_ptr: *const (),
    vptr: *const (),
}
Run Code Online (Sandbox Code Playgroud)

(这与C ++不同,在C ++中,抽象类的vptr存储在对象中。这两种方法都有其优点和缺点。)


自定义DST

实际上,可以通过具有最后一个字段为DST的结构来创建自己的DST。但是,这种情况很少见。一个突出的例子是std::path::Path

指向自定义DST的引用或指针也是胖指针。附加数据取决于结构内部DST的类型。


例外:外部类型

RFC 1861中extern type引入了该功能。外部类型也是DST,但是指向它们的指针不是胖指针。或更确切地说,如RFC所述:

在Rust中,指向DST的指针携带有关所指向对象的元数据。对于字符串和切片,这是缓冲区的长度,对于特征对象,这是对象的vtable。对于外部类型,元数据很简单()。这意味着指向extern类型的指针具有与a相同的大小usize(即,它不是“胖指针”)。

但是,如果您不与C接口交互,则可能永远不必处理这些外部类型。




上面,我们已经看到了不可变引用的大小。胖指针对可变引用,不可变的原始指针和可变的原始指针的作用相同:

size_of::<&[u32]>()       = 16
size_of::<&mut [u32]>()   = 16
size_of::<*const [u32]>() = 16
size_of::<*mut [u32]>()   = 16
Run Code Online (Sandbox Code Playgroud)

  • @nonrectangle 在我的回答中解释这一点将是相当偏离主题的。有关该主题的一些信息:[此处](https://www.reddit.com/r/rust/comments/8ckfdb) 和[此处](https://www.reddit.com/r/ProgrammingLanguages/comments/fpnooj )。如果不存在这样的问题,您也可以考虑在这里询问。 (5认同)
  • @dade 我知道你来自哪里,但不完全是。首先,“String”还有一个“capacity”字段。但更重要的是,如果我们说“胖指针/引用是指向 DST 的指针/引用”,那么“String”就不是这样。那里的指针基本上是`*mut u8`,但是`u8`不是DST。当然,从语义上来说,你是对的。人们可以这样想。然而,从语言律师的意义上来说,答案是否定的。 (5认同)
  • 我想了解更多关于 C++ 和 Rust 存储 `vtbl` 和 `vptr` 方式之间的“优点和缺点”的信息。 (3认同)
  • @nonsquare“胖指针”并不是真正的指针,只是一个内部带有指针的结构。在 C++ 中,一切都是手动的 - 您可以将 vtable 放入对象中,或者传递像 Rust 这样的特征(这在源自 C 的库中很流行),但您始终必须自己定义“胖指针”结构! (2认同)