Question

Rust Pointers Explained: Box, ref, References (&), and Raw Pointers (*)

rustpointers

Question

I am confused about pointer-related syntax in Rust: Box, ref, &, and *. How do these features relate to ownership, borrowing, heap allocation, and dereferencing?

For example:

let thing: Option<i32> = Some(4);

match thing {
    None => println!("none!"),
    Some(ref x) => println!("{}", x),
}

I understand that Box allocates a value on the heap, ref can borrow a value in a pattern, and & creates a reference. However, I am unclear about raw pointers such as *const i32, when they are useful, how they can be used safely, and why * is also used to dereference values. Could you explain these concepts, correct any misunderstandings, and describe common use cases?

Short Answer

Rust has several related but distinct features: Box<T> is an owning smart pointer for heap-allocated data, &T and &mut T are checked references, ref is pattern syntax that creates a reference, and *const T / *mut T are raw pointers for low-level interoperation. The * operator dereferences pointer-like values, but raw-pointer dereferencing requires unsafe.

Concept

Box, ref, &, and * are not four versions of the same pointer type.

  • Box<T> is an owning smart pointer. It owns a T, usually stored on the heap. When the box is dropped, its value is dropped too.
  • &T is a shared reference: many readers are allowed, but no mutation through those references.
  • &mut T is an exclusive mutable reference: it permits mutation, and Rust prevents conflicting access while it exists.
  • ref is pattern syntax, not a type. In a pattern, it asks Rust to bind a reference to the matched value instead of moving it.
  • *const T and *mut T are raw pointers. They can represent memory addresses without Rust's usual validity, lifetime, aliasing, and nullability guarantees.
  • *value is the dereference operator. It accesses the value behind a reference, smart pointer, or raw pointer.

The important distinction is ownership. A Box<T> owns data; a reference borrows data; a raw pointer merely holds an address. Rust's ordinary references are safe because the compiler checks how long they live and whether access rules are followed. Raw pointers opt out of those checks, so accessing the pointee requires an unsafe block.

Box<T> matters because some values need stable heap storage, recursive data structures need indirection, and trait objects often need a known-sized wrapper such as Box<dyn Trait>. It is still a pointer: a Box<T> value is a small handle that points to its owned heap allocation.

Mental Model

Think of memory as a storage facility.

  • A value such as String is an item you own.
  • Box<T> is a storage locker contract: you own the item in that locker and are responsible for it. You can hand the contract to someone else, transferring ownership.
  • &T is a visitor pass: someone may inspect an item without owning it.
  • &mut T is an exclusive maintenance pass: one person may change the item, with no competing visitors.
  • ref is an instruction used while unpacking a package: “do not take the item out; give me a visitor pass to it instead.”
  • A raw pointer is an unverified street address written on paper. It may be correct, outdated, null, or point somewhere inappropriate. Rust requires extra care before you use that address to access anything.

Dereferencing with * means following a handle, pass, or address to reach the item it refers to.

Take Quiz

Syntax and Examples

References and dereferencing

let number = 10;
let reference = &number; // reference: &i32

println!("{reference}");  // Display can print a reference's target
println!("{}", *reference); // Explicitly read the i32 behind &i32

&number borrows number; it does not move it. *reference dereferences the &i32 and produces access to the underlying i32.

Mutable references

let mut score = 10;
let score_ref = &mut score;

*score_ref += 5;
println!("{score}"); // 15

The dereference is necessary because score_ref is the reference itself. *score_ref means the i32 it refers to.

Box: an owning smart pointer

let boxed_number = Box::new(42); // Box<i32>

println!("{}", *boxed_number);

Box::new stores 42 in a heap allocation and returns a Box<i32> that owns it. Box<T> implements Deref, so Rust often dereferences it automatically in method calls and many expressions.

ref in patterns

let message = Some(String::from("hello"));

match message {
    Some(ref text) => println!("Message: {text}"),
    None => println!("No message"),
}

println!("Still available: {:?}", message);

Without ref, the pattern Some(text) would move the String out of message. Some(ref text) instead makes text have type &String, so message remains usable after the match.

In modern Rust, matching a reference is often clearer:

let message = Some(String::from("hello"));

match &message {
    Some(text) => println!("Message: {text}"),
    None => println!("No message"),
}

Raw pointers

let value = 4;
let raw: *const i32 = &value;

unsafe {
    println!("{}", *raw);
}

Creating raw is safe because it only converts a reference into an address. Dereferencing raw is unsafe because the compiler cannot prove that the pointer is non-null, valid, aligned, and still points to a live i32.

Step by Step Execution

Consider a non-Copy value, where borrowing in a pattern makes a visible difference:

let item = Some(String::from("map"));

match item {
    Some(ref name) => println!("Found: {name}"),
    None => println!("Nothing found"),
}

println!("Original value: {:?}", item);
  1. String::from("map") creates an owned String.
  2. Some(...) stores that string inside item, whose type is Option<String>.
  3. The match examines item.
  4. Some(ref name) matches the Some variant but binds name as &String, rather than moving the String.
  5. println! reads the string through name.
  6. The match ends and the borrow ends.
  7. item is still intact, so the final println! is valid.
  8. At the end of the surrounding scope, item is dropped, which drops its contained String.

If the pattern were Some(name), then name would be a String moved out of item. The final use of item would fail to compile.

Note that the original example used i32. Since i32 implements Copy, matching Some(x) copies the integer rather than moving it, so ref is not necessary for ownership preservation in that specific case. Also, a pattern binding such as x exists only inside its match arm, so it cannot be used after the match.

Real World Use Cases

  • Box<T> for recursive data: A linked list or syntax tree cannot directly contain itself because its size would be infinite. Box provides a fixed-size pointer-sized link.

    enum List {
        Node(i32, Box<List>),
        End,
    }
    
  • Box<dyn Trait> for runtime polymorphism: Store different concrete types behind one interface when the concrete size is not known through the trait alone.

    trait Render {
        fn render(&self);
    }
    
    let widget: Box<dyn Render> = Box::new(Button {});
    
  • References for function inputs: Borrow configuration, request data, collections, or strings when a function only needs temporary access.

    fn is_admin(role: &str) -> bool {
        role == "admin"
    }
    
  • Mutable references for in-place changes: Update an existing buffer, append to a string, or modify a struct without allocating a replacement.

  • Pattern references: Inspect an Option<String>, Result<T, E>, enum, or struct field without taking ownership of its contents.

  • Raw pointers for FFI: Call C APIs, receive buffers from operating-system libraries, work with hardware or embedded memory, and implement low-level data structures. Safe wrapper APIs should usually hide the raw pointers from ordinary callers.

Take Quiz

Real Codebase Usage

Developers generally use references and Box in application code, reserving raw pointers for narrow low-level boundaries.

Borrow function inputs

Use &T when a function only reads a value and should not take it from its caller.

fn display_user(name: &str) {
    println!("User: {name}");
}

Prefer &str for read-only text input rather than &String, because both string literals and String values can be used as &str.

Mutate through an explicit borrow

fn add_tag(tags: &mut Vec<String>, tag: String) {
    tags.push(tag);
}

The &mut type advertises that the function changes the caller's collection.

Use pattern matching without moving data

fn log_error(result: &Result<String, String>) {
    if let Err(message) = result {
        eprintln!("Request failed: {message}");
    }
}

Matching &Result<...> avoids moving either variant's contents. This is often preferable to adding ref within the pattern.

Return heap-owned trait objects when needed

trait Storage {
    fn read(&self, key: &str) -> String;
}

fn build_storage() -> Box<dyn Storage> {
    Box::new(MemoryStorage::new())
}

Box gives the return value a known size while preserving dynamic dispatch.

Encapsulate unsafe raw-pointer work

A codebase commonly places raw-pointer operations in a small unsafe implementation and exposes a safe interface. The safe interface should enforce the assumptions required by the unsafe code, such as valid lengths, non-null data, and correct ownership.

Common Mistakes

Treating ref as a standalone pointer type

ref only works in patterns. It is not the same syntax as &.

// Invalid: `ref` is not an expression for making a borrow.
// let borrowed = ref value;

let borrowed = &value;

Use & in expressions and types. Use ref only when binding a value in a pattern.

Assuming ref is needed for Copy values

let value = Some(4);

match value {
    Some(number) => println!("{number}"),
    None => {}
}

println!("{:?}", value); // Valid: i32 is Copy

i32 is copied. For String, Vec<T>, and many custom types, a normal pattern binding moves the value unless you borrow it.

Using a match binding outside its arm

let value = Some(4);

match value {
    Some(x) => println!("{x}"),
    None => {}
}

// println!("{x}"); // Invalid: x existed only in the Some arm.

Bindings created by a match arm are scoped to that arm.

Thinking Box<T> is only for trait objects

Box<T> is useful for any heap-owned T, especially recursive structures or large values whose ownership must be transferred. Trait objects are one important use case, not its only purpose.

Dereferencing a raw pointer without unsafe

let number = 7;
let pointer = &number as *const i32;

// println!("{}", *pointer); // Compile error

Raw pointer access requires unsafe. Adding unsafe is not a guarantee of correctness; it is your promise that the pointer is valid to dereference.

Returning a raw pointer to a local value

// Broken: the local value is dropped when this function returns.
// fn bad_pointer() -> *const i32 {
//     let number = 7;
//     &number as *const i32
// }

The returned pointer would dangle. Prefer returning an owned value, Box<T>, or a reference whose lifetime is tied to valid caller-owned data.

Confusing *const T with an immutable reference

*const T does not mean Rust has verified that the pointee is immutable or valid. It means you should not write through that pointer directly. The same memory could still be changed through another route, subject to the rules of the code using it.

Comparisons

FeatureExampleOwns the value?Safe to dereference?Typical purpose
Shared reference&TNoYesRead borrowed data
Mutable reference&mut TNoYesExclusively modify borrowed data
Owning smart pointerBox<T>YesYesHeap allocation, recursive data, trait objects
Raw const pointer*const TNoOnly in unsafeFFI and low-level address handling
Raw mutable pointer*mut TNoOnly in unsafeFFI and low-level mutation
Pattern binding syntaxSome(ref x)Nox is a safe referenceBorrow while destructuring

&T versus *const T

A reference must point to a valid, properly aligned T for its lifetime. It cannot be null. Rust tracks its lifetime and borrowing rules.

A raw pointer may be null, dangling, unaligned, or invalid. It carries much less compiler-checked information, which is why dereferencing it is unsafe.

Box<T> versus &T

A Box<T> owns the data, so it can outlive the scope that created it when ownership is moved elsewhere. A &T only borrows data, so it cannot outlive the owner it references.

ref x versus &x in a pattern

Both can create a reference during matching, but their pattern shapes differ:

let option = Some(String::from("rust"));

match option {
    Some(ref text) => println!("{text}"),
    None => {}
}

match &option {
    Some(text) => println!("{text}"),
    None => {}
}

The first matches an Option<String> and borrows its inner field. The second borrows the entire Option<String> first, then matches an &Option<String>.

Take Quiz

Cheat Sheet

  • Box<T>: owns a T stored on the heap.

    let value = Box::new(5);
    
  • &T: shared borrow; read-only through this reference.

    let reference = &value;
    
  • &mut T: exclusive mutable borrow.

    let reference = &mut value;
    *reference += 1;
    
  • ref name: pattern-only syntax that binds name as a reference.

    Some(ref name) => println!("{name}")
    
  • *const T: raw pointer intended for read access.

    let pointer: *const i32 = &number;
    
  • *mut T: raw pointer that may be used for mutation.

  • *pointer: dereference operator.

    • Safe for valid &T, &mut T, and Box<T>.
    • Requires unsafe for *const T and *mut T.
  • Raw pointer safety requirements before dereferencing:

    • The pointer is non-null.
    • It points to live memory.
    • The memory is correctly aligned for T.
    • Access follows aliasing and mutation rules.
  • i32, bool, and similar simple types are often Copy; String and Vec<T> are not. Pattern moves matter most for non-Copy values.

FAQ

Is Box<T> a pointer in Rust?

Yes. It is an owning smart pointer: it stores a pointer to heap-allocated T data and manages that allocation automatically.

Does ref create a reference?

Yes, but only in a pattern. For example, Some(ref value) makes value an &T when matching Option<T>.

Why is * used for both raw pointers and dereferencing?

Rust uses *const T and *mut T in type syntax to describe raw pointers. In an expression, *pointer means dereference: access the value a pointer-like value refers to.

Can I dereference a Box<T> without unsafe?

Yes. Box<T> guarantees ownership of a valid allocation and implements safe dereferencing behavior.

Why does dereferencing a raw pointer require unsafe?

The compiler cannot verify whether a raw pointer is valid, non-null, aligned, or still points to live data. The programmer must uphold those conditions.

Should I use raw pointers instead of references for performance?

Usually no. References are idiomatic, checked by the compiler, and generally optimize well. Use raw pointers when a low-level API or implementation requirement specifically needs them.

Is Some(ref x) always necessary to avoid moving a value?

No. You can often match a reference instead: match &option { Some(x) => ... }. Also, types implementing Copy, such as i32, are copied rather than moved.

Does &self always expire exactly when a method returns?

Usually the borrow is used for the method call and ends when no longer needed, often at the end of the call. Rust uses lifetimes and non-lexical lifetimes to determine the actual valid borrow duration.

Take Quiz

Mini Project

Description

Build a small inventory lookup program that stores item names in Option<String>. It demonstrates borrowing an optional value with pattern matching, mutating borrowed data through &mut, and storing a recursive category tree with Box<T>.

Goal

Inspect and update inventory data without accidentally moving owned strings, then print a boxed recursive category tree.

Requirements

Create an Option<String> containing an item name. Borrow the item name while checking whether it is present. Update a quantity through a mutable reference. Define and use a recursive enum containing Box<T>. Print output that confirms the original item name remains available.

Take Quiz

Keep learning

Related questions

View more