Question

What Is a Fat Pointer in Rust? Slices, Trait Objects, and Why They Are Larger

rustpointers

Question

I have seen the term fat pointer used in several Rust discussions, but I am not sure what it means or when Rust uses one. It appears to be twice the size of a normal pointer, and I do not understand why. I have also noticed that it is often mentioned together with trait objects. What exactly is a fat pointer in Rust, and why is it needed?

Short Answer

By the end of this page, you will understand what a fat pointer is in Rust, why it stores more than just a memory address, and where it appears in everyday Rust code. You will see how Rust uses fat pointers for slices, string slices, and trait objects, and how that extra information makes dynamically sized data usable and safe.

Concept

A fat pointer in Rust is a pointer value that contains more than just an address.

A normal pointer is often called a thin pointer. It usually stores only:

  • the address of some value in memory

A fat pointer stores:

  • the address of the value
  • extra metadata needed to use that value correctly

Why Rust needs fat pointers

Some Rust types do not have enough information available from an address alone.

For example:

  • A slice like &[i32] points to a sequence of i32 values, but the address alone does not tell Rust how many elements are in the slice.
  • A trait object like &dyn Display points to some value implementing a trait, but the address alone does not tell Rust which methods to call for the concrete type.

So Rust attaches metadata to the pointer.

Common fat pointer cases

1. Slices

A slice such as:

let nums: &[i32] = &[10, 20, 30];

is a fat pointer containing:

  • a pointer to the first element
  • the length of the slice

This is why Rust can safely allow:

nums.len()

and bounds-checked indexing.

2. String slices

A string slice like &str is also a fat pointer containing:

  • a pointer to the first byte
  • the length in bytes

3. Trait objects

A trait object such as:

let x: &dyn std::fmt::Display = &42;

is a fat pointer containing:

  • a pointer to the actual value
  • a pointer to a vtable

The vtable is a table of function pointers and metadata that tells Rust how to use the concrete type through the trait.

Why they are often twice as large

On a 64-bit system, a normal pointer is usually 8 bytes. A fat pointer usually has:

  • one pointer-sized field for the data address
  • one pointer-sized field for the metadata

So it is often 16 bytes total.

That is why people say a fat pointer is often "twice as large" as a normal pointer.

Important idea: fat pointers are about dynamically sized or dynamically dispatched data

Rust uses fat pointers when the compiler needs runtime metadata to work with something behind a reference or pointer.

This happens mainly with:

  • DSTs (dynamically sized types), such as [T] and str
  • trait objects, such as dyn Trait

These types cannot be fully described by an address alone.

Mental Model

Think of a normal pointer as a street address written on a note.

That is enough if you already know exactly what is at that address and how large it is.

A fat pointer is like a street address plus instructions.

For slices, the instructions say:

  • start here
  • read N items

For trait objects, the instructions say:

  • the value is here
  • use this method table to know which functions belong to this concrete type

So a fat pointer is not "fat" because it points to more data. It is "fat" because it carries extra information needed to interpret the data correctly.

Take Quiz

Syntax and Examples

Core syntax

Slice fat pointer

let array = [1, 2, 3, 4];
let slice: &[i32] = &array[1..3];

slice is a fat pointer:

  • pointer to array[1]
  • length 2

String slice fat pointer

let text: &str = "hello";

text is a fat pointer:

  • pointer to the UTF-8 bytes
  • byte length 5

Trait object fat pointer

use std::fmt::Display;

let value: &dyn Display = &123;
println!("{}", value);

value is a fat pointer:

  • pointer to the integer 123
  • pointer to a vtable for i32 as Display

Example: size difference

use std::mem::size_of;

fn main() {
    println!("size of &i32: {}", size_of::<&i32>());
    println!("size of &[i32]: {}", size_of::<&[i32]>());
    println!("size of &str: {}", size_of::<&str>());
    println!("size of &dyn ToString: {}", size_of::<&dyn ToString>());
}

On a 64-bit machine, you will commonly see something like:

size of &i32: 8
size of &[i32]: 16
size of &str: 16
size of &dyn ToString: 16

This happens because:

  • &i32 is thin: address only
  • &[i32] is fat: address + length
  • &str is fat: address + length
  • &dyn ToString is fat: address + vtable pointer

Step by Step Execution

Consider this example:

use std::fmt::Display;

fn show(item: &dyn Display) {
    println!("{}", item);
}

fn main() {
    let number = 42;
    show(&number);
}

Here is what happens step by step:

  1. number is created as an i32 value.
  2. &number creates a reference to that i32.
  3. The function show expects &dyn Display, not &i32.
  4. Rust converts &i32 into a trait object reference &dyn Display.
  5. That trait object reference becomes a fat pointer containing:
    • the address of number
    • a vtable pointer for i32 implementing Display
  6. Inside show, println!("{}", item) uses the vtable to call the correct formatting code for i32.

Slice example trace

fn main() {
    let data = [10, 20, 30, 40];
    let part = &data[1..3];

    println!("len = {}", part.len());
    println!("first = {}", part[0]);
}

Step by step:

  1. data is an array of 4 integers.
  2. &data[1..3] creates a slice covering 20 and 30.
  3. The slice part stores:
    • the address of data[1]
    • the length 2
  4. part.len() reads the stored length metadata.
  5. part[0] means "start from the stored address and read the first element in this 2-element slice."

Real World Use Cases

Fat pointers appear in many practical Rust programs.

Working with parts of collections

Slices are used constantly for:

  • passing part of an array to a function
  • returning borrowed views into data
  • avoiding copies

Example:

fn sum(values: &[i32]) -> i32 {
    values.iter().sum()
}

The function does not need ownership of a Vec<i32>. It only needs a slice.

Handling text efficiently

&str is used everywhere for:

  • function parameters
  • parsing input
  • reading config values
  • borrowing text without allocating

Example:

fn greet(name: &str) {
    println!("Hello, {name}!");
}

Dynamic dispatch with trait objects

Trait objects are useful when code should accept different concrete types through a shared interface.

Examples:

  • GUI components implementing a Draw trait
  • logging backends
  • plugin-like systems
  • heterogeneous collections using Box<dyn Trait>

Example:

trait Animal {
    fn speak(&self);
}

struct Dog;
struct Cat;

impl Animal for Dog {
    fn speak(&self) { println!("woof"); }
}

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

fn make_it_speak(animal: &dyn Animal) {
    animal.speak();
}

Here, &dyn Animal is a fat pointer because Rust needs runtime method lookup.

Take Quiz

Real Codebase Usage

In real Rust projects, developers rarely talk about fat pointers directly every day, but they use the types built on them all the time.

Common patterns

Accepting slices instead of concrete containers

fn process(ids: &[u64]) {
    for id in ids {
        println!("processing {id}");
    }
}

This makes APIs flexible because callers can pass:

  • arrays
  • vectors
  • subslices

Accepting &str instead of String

fn is_valid_name(name: &str) -> bool {
    !name.trim().is_empty()
}

This avoids unnecessary allocation and works with both string literals and owned strings.

Using trait objects for runtime polymorphism

trait Handler {
    fn handle(&self, input: &str);
}

fn run(handler: &dyn Handler, input: &str) {
    if input.is_empty() {
        return;
    }

    handler.handle(input);
}

This pattern is common in:

  • command systems
  • event processing
  • middleware
  • dependency injection-like designs

Guard clauses and validation

Fat-pointer-based types often appear in clean API design:

fn first_word(text: &str) -> &str {
    if text.is_empty() {
        return "";
    }

    match text.find(' ') {
        Some(index) => &text[..index],
        None => text,
    }
}

This uses &str slices to return borrowed parts of a string efficiently.

Why this matters in codebases

Understanding fat pointers helps explain:

  • why &str and &[T] are lightweight borrowed views
  • why dyn Trait supports runtime dispatch
  • why some types have no compile-time known size by themselves
  • why references to DSTs carry metadata

Common Mistakes

1. Thinking the pointer points to two separate objects

A fat pointer is one pointer value with two pieces of information. It does not mean there are two heap allocations.

For example, &str does not mean:

  • one pointer to text
  • one separate owned object for the length

The length is metadata stored in the reference value itself.

2. Assuming all references are fat pointers

This is false.

let x = 10;
let a: &i32 = &x;      // thin pointer
let b: &[i32] = &[1,2,3]; // fat pointer

Only references to DSTs or trait objects are fat.

3. Confusing slices with arrays

Broken idea:

let arr: [i32] = [1, 2, 3];

This is only valid when the size is known at compile time, such as [i32; 3].

Correct:

let arr: [i32; 3] = [1, 2, 3];
let slice: &[i32] = &arr;

The array has fixed size. The slice is a fat pointer view over it.

4. Thinking dyn Trait stores the object inline

Broken mental model:

let x: dyn std::fmt::Display; // invalid by itself

Trait objects are DSTs, so they are usually used behind a pointer such as:

let x: &dyn std::fmt::Display = &42;

or

let x: Box<dyn std::fmt::Display> = Box::new(42);

5. Mixing up static and dynamic dispatch

If you write:

fn print_it<T: std::fmt::Display>(value: T) {
    println!("{}", value);
}

this uses generics and typically static dispatch, not a trait object fat pointer.

If you write:

fn print_it(value: &dyn std::fmt::Display) {
    println!("{}", value);
}

this uses a trait object fat pointer and dynamic dispatch.

Comparisons

ConceptStoresSized at compile time?Common examplesDispatch/style
Thin pointerAddress onlyYes&i32, *const T to sized TDirect access
Fat pointer to sliceAddress + lengthPointer is sized, pointee is DST&[i32], &strRuntime length metadata
Fat pointer to trait objectAddress + vtable pointerPointer is sized, pointee is DST&dyn Display, Box<dyn Write>Dynamic dispatch

Generics vs trait objects

FeatureGenerics (T: Trait)Trait objects (dyn Trait)
DispatchUsually staticDynamic
PerformanceOften more optimizableSmall runtime indirection
Code sizeCan increase due to monomorphizationOften smaller shared code
Pointer typeOften thin for sized TFat pointer
Use whenConcrete type can stay genericNeed runtime flexibility

Arrays vs slices

TypeSize known at compile time?Owns data?Fat pointer?
[T; N]YesYes, inlineNo
[T]NoNot by itselfDST, used behind a fat pointer
&[T]YesNoYes
Take Quiz

Cheat Sheet

Quick reference

  • Thin pointer = address only
  • Fat pointer = address + metadata

Common Rust fat pointers

&[T]        // pointer + length
&mut [T]    // pointer + length
&str        // pointer + byte length
&dyn Trait  // pointer + vtable
Box<dyn Trait> // box pointer structure for trait object use

Why fat pointers exist

They let Rust work with values where the address alone is not enough.

Dynamically sized types involved

  • [T]
  • str
  • dyn Trait

These are usually used behind a pointer.

Size intuition on a 64-bit machine

&i32      -> 8 bytes
&[i32]    -> 16 bytes
&str      -> 16 bytes
&dyn Trait -> 16 bytes

Key idea

  • Slice metadata = length
  • Trait object metadata = vtable pointer

Useful check

use std::mem::size_of;

println!("{}", size_of::<&i32>());
println!("{}", size_of::<&str>());

Important rule

The pointer value is fat, not necessarily the underlying object.

Edge case to remember

A DST such as str or dyn Trait cannot usually stand alone by value in normal code. It must be accessed through a pointer-like type.

FAQ

What is the difference between a thin pointer and a fat pointer in Rust?

A thin pointer stores only a memory address. A fat pointer stores a memory address plus metadata, such as a slice length or a trait object vtable pointer.

Is &str a fat pointer in Rust?

Yes. &str stores a pointer to UTF-8 bytes and the length in bytes.

Is &[T] a fat pointer?

Yes. A slice reference stores a pointer to the first element and the number of elements in the slice.

Why does &dyn Trait need a vtable?

Because the concrete type is not known through the trait object alone. The vtable tells Rust which method implementations to call at runtime.

Are fat pointers always 16 bytes?

Not always. They are often two machine words, so on a 64-bit system they are commonly 16 bytes. This depends on the target architecture.

Does a fat pointer mean extra heap allocation?

No. The extra metadata is part of the pointer value itself, not necessarily a separate allocation.

Are Box<dyn Trait> and &dyn Trait both related to fat pointers?

Yes. Both represent trait objects and carry the metadata needed for dynamic dispatch.

When should I care about fat pointers in Rust?

You should care when working with slices, string slices, trait objects, dynamically sized types, API design, and understanding static versus dynamic dispatch.

Take Quiz

Mini Project

Description

Build a small Rust program that demonstrates where fat pointers appear in normal code. The program will compare pointer sizes and then use both a slice and a trait object. This helps connect the idea of "extra metadata" with practical Rust types you already use.

Goal

Create a program that prints the sizes of several reference types and uses a slice plus a trait object in working examples.

Requirements

  • Print the size of &i32, &[i32], &str, and &dyn Display.
  • Create an array and pass a slice of it to a function.
  • Create a function that accepts &dyn Display and prints the value.
  • Keep the program simple and runnable as a single main.rs file.
Take Quiz

Keep learning

Related questions

View more