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 ofi32values, but the address alone does not tell Rust how many elements are in the slice. - A trait object like
&dyn Displaypoints 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]andstr - 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
Nitems
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.
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
i32asDisplay
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:
&i32is thin: address only&[i32]is fat: address + length&stris fat: address + length&dyn ToStringis 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:
numberis created as ani32value.&numbercreates a reference to thati32.- The function
showexpects&dyn Display, not&i32. - Rust converts
&i32into a trait object reference&dyn Display. - That trait object reference becomes a fat pointer containing:
- the address of
number - a vtable pointer for
i32implementingDisplay
- the address of
- Inside
show,println!("{}", item)uses the vtable to call the correct formatting code fori32.
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:
datais an array of 4 integers.&data[1..3]creates a slice covering20and30.- The slice
partstores:- the address of
data[1] - the length
2
- the address of
part.len()reads the stored length metadata.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
Drawtrait - 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.
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
&strand&[T]are lightweight borrowed views - why
dyn Traitsupports 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
| Concept | Stores | Sized at compile time? | Common examples | Dispatch/style |
|---|---|---|---|---|
| Thin pointer | Address only | Yes | &i32, *const T to sized T | Direct access |
| Fat pointer to slice | Address + length | Pointer is sized, pointee is DST | &[i32], &str | Runtime length metadata |
| Fat pointer to trait object | Address + vtable pointer | Pointer is sized, pointee is DST | &dyn Display, Box<dyn Write> | Dynamic dispatch |
Generics vs trait objects
| Feature | Generics (T: Trait) | Trait objects (dyn Trait) |
|---|---|---|
| Dispatch | Usually static | Dynamic |
| Performance | Often more optimizable | Small runtime indirection |
| Code size | Can increase due to monomorphization | Often smaller shared code |
| Pointer type | Often thin for sized T | Fat pointer |
| Use when | Concrete type can stay generic | Need runtime flexibility |
Arrays vs slices
| Type | Size known at compile time? | Owns data? | Fat pointer? |
|---|---|---|---|
[T; N] | Yes | Yes, inline | No |
[T] | No | Not by itself | DST, used behind a fat pointer |
&[T] | Yes | No | Yes |
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]strdyn 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.
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 Displayand prints the value. - Keep the program simple and runnable as a single
main.rsfile.
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.