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 aT, usually stored on the heap. When the box is dropped, its value is dropped too.&Tis a shared reference: many readers are allowed, but no mutation through those references.&mut Tis an exclusive mutable reference: it permits mutation, and Rust prevents conflicting access while it exists.refis pattern syntax, not a type. In a pattern, it asks Rust to bind a reference to the matched value instead of moving it.*const Tand*mut Tare raw pointers. They can represent memory addresses without Rust's usual validity, lifetime, aliasing, and nullability guarantees.*valueis 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
Stringis 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.&Tis a visitor pass: someone may inspect an item without owning it.&mut Tis an exclusive maintenance pass: one person may change the item, with no competing visitors.refis 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.
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);
String::from("map")creates an ownedString.Some(...)stores that string insideitem, whose type isOption<String>.- The
matchexaminesitem. Some(ref name)matches theSomevariant but bindsnameas&String, rather than moving theString.println!reads the string throughname.- The match ends and the borrow ends.
itemis still intact, so the finalprintln!is valid.- At the end of the surrounding scope,
itemis dropped, which drops its containedString.
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.Boxprovides 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.
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
| Feature | Example | Owns the value? | Safe to dereference? | Typical purpose |
|---|---|---|---|---|
| Shared reference | &T | No | Yes | Read borrowed data |
| Mutable reference | &mut T | No | Yes | Exclusively modify borrowed data |
| Owning smart pointer | Box<T> | Yes | Yes | Heap allocation, recursive data, trait objects |
| Raw const pointer | *const T | No | Only in unsafe | FFI and low-level address handling |
| Raw mutable pointer | *mut T | No | Only in unsafe | FFI and low-level mutation |
| Pattern binding syntax | Some(ref x) | No | x is a safe reference | Borrow 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>.
Cheat Sheet
-
Box<T>: owns aTstored 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 bindsnameas 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, andBox<T>. - Requires
unsafefor*const Tand*mut T.
- Safe for valid
-
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 oftenCopy;StringandVec<T>are not. Pattern moves matter most for non-Copyvalues.
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.
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.
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.