Fat Pointers in Rust: Understanding Fat Pointers

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

What's Behind the "Fat" Pointer? An Analysis of Fat Pointers in Rust

\n\n

Imagine you go to a library and ask for a book. The librarian gives you a small slip of paper with the shelf and row number — that's a regular pointer. But what if you don't know the size of the book? Or what if it's a three-volume set? You'll need a second slip: "Volume 1 of 3" or "Length: 450 pages".

\n\n

In Rust, such a "double" pointer is called a fat pointer. It's not magic, but simply a pair of numbers: the data address and its meta-information (most often the length or a pointer to a virtual method table). Let's figure out why this is needed and how it works, using real code examples.

\n\n

Regular Pointers: Thin and Fast

\n\n

Let's start with the basics. Rust has simple pointers — &T (reference) or *const T (raw pointer). They take exactly 8 bytes (on a 64-bit system) and store only the memory address.

\n\n
fn main() {\n    let x: i32 = 42;\n    let ptr: &i32 = &x; // thin pointer — 8 bytes\n    println!("Size of reference: {} bytes", std::mem::size_of_val(&ptr));\n}\n
\n\n

Output: Size of reference: 8 bytes. Everything is predictable: the compiler knows that i32 is 4 bytes, so no additional information is needed. But what if the data size is unknown in advance?

\n\n

First Fat Pointer: Slices (&[T])

\n\n

A slice is a "window" into an array or vector. But to use it, you need to know not only the beginning but also the number of elements. Rust stores this as a pair (ptr, len) — that's a fat pointer.

\n\n
fn main() {\n    let arr = [1, 2, 3, 4, 5];\n    let slice: &[i32] = &arr[1..4]; // slice of 3 elements\n\n    println!("Size of slice: {} bytes", std::mem::size_of_val(&slice));\n    println!("Elements: {:?}", slice);\n}\n
\n\n

Output: Size of slice: 16 bytes (8 bytes pointer + 8 bytes length). If we only passed the address, we wouldn't know where to stop. That's why a slice is a classic example of a fat pointer.

\n\n

By the way, the string &str works exactly the same way: it's a fat pointer (*const u8, usize). That's why &str takes 16 bytes, while &String takes only 8 (because String itself stores the length internally).

\n\n

Second Fat Pointer: Trait Objects (&dyn Trait)

\n\n

Rust has dynamic dispatch via trait objects. When you write &dyn MyTrait, the compiler doesn't know which concrete type will be passed. It needs a virtual method table (vtable) to call the correct functions.

\n\n
trait Animal {\n    fn speak(&self);\n}\n\nstruct Dog;\nstruct Cat;\n\nimpl Animal for Dog {\n    fn speak(&self) { println!("Woof!"); }\n}\n\nimpl Animal for Cat {\n    fn speak(&self) { println!("Meow!"); }\n}\n\nfn main() {\n    let dog = Dog;\n    let cat = Cat;\n\n    let animals: [&dyn Animal; 2] = [&dog, &cat];\n\n    println!("Size of &dyn Animal: {} bytes", std::mem::size_of::<&dyn Animal>());\n\n    for animal in &animals {\n        animal.speak();\n    }\n}\n
\n\n

Output: Size of &dyn Animal: 16 bytes. These 16 bytes are again a fat pointer: the first 8 bytes are the data address (Dog or Cat), the second 8 bytes are the vtable address (method table).

\n\n

How Does a Fat Pointer Affect Performance?

\n\n

Fat pointers are a trade-off. On one hand, they carry more information, which allows working with dynamic types. On the other hand:

\n\n
    \n
  • Memory: A fat pointer takes 16 bytes instead of 8. This is not critical, but in large collections it can have an impact.
  • \n
  • Speed: Access via vtable (for trait objects) is slightly slower than a direct method call (monomorphization). The diff

Blogs

Book Recommendations