What is a "fat pointer"? Ask Question

What is a

I've read the term "fat pointer" in several contexts already, but I'm not sure what exactly it means and when it is used in Rust. The pointer seems to be twice as large as a normal pointer, but I don't understand why. It also seems to have something to do with trait objects.

ベストアンサー1

The term "fat pointer" is used to refer to references and raw pointers to dynamically sized types (DSTs) – slices or trait objects. A fat pointer contains a pointer plus some information that makes the DST "complete" (e.g. the length).

Most commonly used types in Rust are not DSTs but have a fixed size known at compile time. These types implement the Sized trait. Even types that manage a heap buffer of dynamic size (like Vec<T>) are Sized, as the compiler knows the exact number of bytes a Vec<T> instance will take up on the stack. There are currently four different kinds of DSTs in Rust.


Slices ([T] and str)

The type [T] (for any T) is dynamically sized (so is the special "string slice" type str). That's why you usually only see it as &[T] or &mut [T], i.e. behind a reference. This reference is a so-called "fat pointer". Let's check:

dbg!(size_of::<&u32>());
dbg!(size_of::<&[u32; 2]>());
dbg!(size_of::<&[u32]>());

This prints (with some cleanup):

size_of::<&u32>()      = 8
size_of::<&[u32; 2]>() = 8
size_of::<&[u32]>()    = 16

So we see that a reference to a normal type like u32 is 8 bytes large, as is a reference to an array [u32; 2]. Those two types are not DSTs. But as [u32] is a DST, the reference to it is twice as large. In the case of slices, the additional data that "completes" the DST is simply the length. So one could say the representation of &[u32] is something like this:

struct SliceRef { 
    ptr: *const u32, 
    len: usize,
}

Trait objects (dyn Trait)

When using traits as trait objects (i.e. type erased, dynamically dispatched), these trait objects are DSTs. Example:

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>());

This prints (with some cleanup):

size_of::<&Cat>()        = 8
size_of::<&dyn Animal>() = 16

Again, &Cat is only 8 bytes large because Cat is a normal type. But dyn Animal is a trait object and therefore dynamically sized. As such, &dyn Animal is 16 bytes large.

In the case of trait objects, the additional data that completes the DST is a pointer to the vtable (the vptr). I cannot fully explain the concept of vtables and vptrs here, but they are used to call the correct method implementation in this virtual dispatch context. The vtable is a static piece of data that basically only contains a function pointer for each method. With that, a reference to a trait object is basically represented as:

struct TraitObjectRef {
    data_ptr: *const (),
    vptr: *const (),
}

(This is different from C++, where the vptr for abstract classes is stored within the object. Both approaches have advantages and disadvantages.)


Custom DSTs

It's actually possible to create your own DSTs by having a struct where the last field is a DST. This is rather rare, though. One prominent example is std::path::Path.

A reference or pointer to the custom DST is also a fat pointer. The additional data depends on the kind of DST inside the struct.


例外: 外部型

RFC 1861このextern type機能が導入されました。Extern型もDSTですが、それらへのポインタはないファット ポインタ。より正確には、RFC では次のように述べられています。

Rust では、DST へのポインターは、指し示されているオブジェクトに関するメタデータを保持します。文字列とスライスの場合、これはバッファーの長さであり、特性オブジェクトの場合、これはオブジェクトの vtable です。extern 型の場合、メタデータは単に です()。つまり、extern 型へのポインターは と同じサイズになりますusize(つまり、「ファット ポインター」ではありません)。

しかし、C インターフェースとやり取りしていない場合は、おそらくこれらの extern 型を扱う必要はないでしょう。




上記では、不変参照のサイズを見てきました。ファット ポインターは、可変参照、不変生ポインター、可変生ポインターに対して同じように動作します。

size_of::<&[u32]>()       = 16
size_of::<&mut [u32]>()   = 16
size_of::<*const [u32]>() = 16
size_of::<*mut [u32]>()   = 16

おすすめ記事