References as Values
Understand how references behave as values in Rust, their memory representation, assignment semantics, and what makes them different from raw pointers and C++ references.
Rust references feel a lot like pointers if you come from C or C++ — a number that holds a memory address. But in Rust that number is never just a number. Every reference carries compile-time guarantees about validity, aliasing, and lifetime. Those guarantees are enforced by the borrow checker (covered in later sections), but the reference itself is a first-class value with its own rules for copying, comparing, and representing different kinds of data. Understanding how references behave as values is essential before you can reason about ownership and borrowing in any real program.
What a Reference Actually Is
A reference is a value that stores the address of some data — data that lives either on the stack, the heap, or in the static region of the program. Unlike a raw pointer, a Rust reference is bound by the language to be non-null, properly aligned, and to always point to a valid, initialized instance of its type for the entire duration of its use. That means you never check if a &T is null because it simply cannot be.
Under the hood, a reference is a pointer. The machine code generated for &x is identical to what a C compiler would produce for a restricted, non-null pointer. The magic is in the compile-time rules that prevent dangling, double-free, or data-race mistakes before the binary is even produced.
For a beginner, think of a reference as a read-only label stuck on a box. You can have many labels on the same box, and each label knows where the box is. You can look at the box through any label, but you cannot move the box or destroy it while a label is still attached.
References Implement Copy:
References are just pointer-sized numbers, so they implement the Copy trait. When you assign a reference to another variable, the pointer is copied bit-for-bit — the referenced data is not touched. This is safe because the borrow checker ensures the original and the copy both respect the same aliasing rules.
References and Memory Representation
At runtime, the most common reference type &T or &mut T occupies exactly one pointer’s width — 8 bytes on a 64-bit system. The compiler lays out stack frames as usual, and a reference variable just holds the target’s address.
pub struct Point {
pub x: u32,
pub y: u32,
}
let pt = Point { x: 1, y: 2 };
let x: u64 = 0;
let ref_x: &u64 = &x;
let ref_pt: &Point = &pt;
Here ref_x and ref_pt each take up 8 bytes on the stack, storing the addresses of x and pt. The actual data those addresses point to stays wherever it was allocated — on the stack, in this case. If pt were inside a Box<Point>, the reference would hold the heap address, but the reference itself is still a plain stack value.
The compiler treats references as simple pointers, so passing them between functions is cheap: a single 8-byte copy. There is no automatic deep copy, no reference counting, and no garbage collector involvement. The safety rules that govern when a reference may be created and how long it lives are all enforced at compile time and disappear entirely in the final binary.
Rust References Compared to C++ References
If you are familiar with C++, you might see &T and think of a C++ reference. The comparison is useful, but the differences are more important than the similarities.
What they share:
- Both are required to point to a valid object (no null references).
- Both provide a way to access a value indirectly without copying it.
Where Rust diverges:
- Lifetime enforcement. A C++ function can return a dangling reference to a local variable and the compiler will say nothing. Rust rejects that program at compile time.
- Default mutability. A Rust
&Tis immutable by default (equivalent toconst T&in C++). Writable access needs an explicit&mut T. C++ reverses this:T&is mutable unless you addconst. - Aliasing guarantees. Rust’s mutable reference
&mut Tis guaranteed to be unique — no other reference can be used to access the same data while it is alive. C++ has no such guarantee, so data races through aliased mutable references are possible. - No implicit conversions. C++ allows a temporary to bind to a
const T&, extending its lifetime in subtle ways. Rust has a similar mechanism (temporary lifetime extension) but it is stricter and well-defined only forletbindings. - Size and fat pointers. A Rust reference can be a fat pointer for slices (
&[T]) and trait objects (&dyn Trait), carrying extra metadata alongside the address. C++ references are always a single pointer (or are optimised away).
These differences mean that while the syntax looks familiar, the mental model you bring from C++ can mislead you. A Rust reference is more like a statically-checked borrow than a casual alias.
Assigning and Copying References
References themselves are values, and assigning one reference to another variable copies the pointer, not the data it points to. Because all shared references &T and mutable references &mut T implement Copy, the assignment does not move the original — the old variable remains usable.
let data = String::from("hello");
let r1: &String = &data;
let r2: &String = r1; // Copies the pointer, data is not moved.
println!("{r1}"); // Works fine — r1 is still valid.
println!("{r2}"); // Points to the same `data`.
This is a sharp departure from owned values like String, which move on assignment. With references you get cheap, implicit duplication of the address, not a transfer of ownership. The borrow checker ensures that while these copies exist, the rules of borrowing (no mutable alias coexists with a shared reference, etc.) are obeyed, but the copies themselves are harmless.
Copy Can Hide Lifetime Issues:
Because references are Copy, you might accidentally hold onto a reference longer than intended if you pass it into a scope that retains a copy. The compiler’s lifetime tracking catches misuse, but beginners often assume that copying a reference resets its lifetime, which it does not. Every copy is bound by the same lifetime as the original borrow.
References to References
Rust allows you to take a reference to a reference itself, producing types like &&T, &mut &T, or &mut &mut T. These are real values: a pointer to a stack slot that holds a pointer to the actual data.
let mut value: i32 = 10;
let mut r1: &i32 = &value;
let r2: &mut &i32 = &mut r1; // r2 is a mutable reference to the reference r1.
// Through r2 we can change which value r1 points to.
let other = 20;
*r2 = &other; // r1 now points to `other`.
println!("{r1}"); // Prints 20.
The type &mut &i32 reads right-to-left: a mutable reference to a shared reference to an i32. Dereferencing r2 gives &i32 — a reference, not the integer. This can be confusing because the dereference depth matters. A common mistake is to try to assign a value through a reference to a reference as if it were a direct mutable reference to the integer.
Dereferencing Through Layers:
If you have &mut &i32, dereferencing once gives you the inner &i32, which is shared and cannot be used to mutate the integer. To mutate the integer directly, you need &mut i32. Understand how many layers of indirection you have before applying *.
Comparing References
When you compare two references with ==, Rust compares the values they point to, not the addresses themselves. The standard library provides a blanket implementation of PartialEq for &T where T: PartialEq, which dereferences both sides and compares the underlying data.
let a = 5;
let b = 5;
let ra = &a;
let rb = &b;
// Compares the integers, not the addresses.
assert!(ra == rb); // true because 5 == 5.
To compare pointer addresses — checking whether two references point to the exact same memory location — you must use std::ptr::eq.
let x = 10;
let rx1 = &x;
let rx2 = &x;
assert!(std::ptr::eq(rx1, rx2)); // true: same address.
assert!(rx1 == rx2); // also true: same value.
let y = 10;
let ry = &y;
assert!(!std::ptr::eq(rx1, ry)); // different address.
assert!(rx1 == ry); // true: equal values.
The automatic dereference in == is convenient but can mask whether you are comparing identity or content. When debugging or writing data structure internals, use the explicit pointer comparison to avoid ambiguity.
References Are Never Null
A Rust reference of type &T or &mut T is guaranteed by the compiler to point to a valid, initialized value. There is no null value in safe Rust. If an operation can legitimately fail to produce a reference, you use Option<&T> — None serves the role that a null pointer would play in other languages.
fn find_even(numbers: &[i32]) -> Option<&i32> {
numbers.iter().find(|&&n| n % 2 == 0)
}
This function returns Some(&even_number) or None. It is impossible to accidentally dereference a None value because you must pattern-match or use combinators on the Option. The cost of this safety is zero at runtime for the reference itself; the Option adds a discriminant, but no null checks on the pointer are ever emitted because the compiler knows the reference inside Some is valid.
This property eliminates an entire class of crashes. You never have to write defensive checks like if (ptr != NULL) in safe Rust. If you hold a &T, the data is there.
No Null Checks at Runtime:
Since a reference cannot be null, the compiler never inserts runtime null checks when you dereference. The absence of null is enforced entirely at compile time, so the generated code is both safe and fast.
Borrowing References to Arbitrary Expressions
You can take a reference to a temporary value produced by any expression. Rust guarantees that the temporary stays alive at least until the end of the enclosing statement. This lets you write compact code without manually naming every intermediate result.
struct Point { x: i32, y: i32 }
fn distance_from_origin(p: &Point) -> f64 {
((p.x.pow(2) + p.y.pow(2)) as f64).sqrt()
}
// Temporary Point lives until the semicolon.
let d = distance_from_origin(&Point { x: 3, y: 4 });
println!("Distance: {d}");
Here &Point { x: 3, y: 4 } creates an unnamed Point on the stack and takes a reference to it. The temporary is held alive for the duration of the let statement, so the reference is valid inside distance_from_origin. As soon as the statement finishes, the temporary is dropped.
Reference to a Temporary Must Not Escape:
You cannot store a reference to a temporary in a variable that outlives the statement. If you try to assign &Point { x: 3, y: 4 } to a let binding that lives longer, the compiler rejects it. Temporary lifetime extension applies only in specific let patterns.
Fat Pointer References: Slices and Trait Objects
Most references are plain, 8-byte pointers. But two reference types carry extra metadata, making them twice as wide — 16 bytes on a 64-bit platform. These are called fat pointers.
References to Slices: &[T]
A slice reference &[T] describes a view into a contiguous sequence of T values. It contains both a pointer to the first element and a usize length. The length enables safe iteration and bounds checking without needing to know the size of the underlying container at compile time.
let arr: [i32; 5] = [10, 20, 30, 40, 50];
let slice: &[i32] = &arr[1..4]; // Pointer to arr[1], length 3.
assert_eq!(slice.len(), 3);
assert_eq!(slice[0], 20);
A slice reference can point into a stack-allocated array, a Vec’s heap allocation, or even a static region — the fat pointer does not care where the memory lives. The length field makes &[T] a dynamically sized type behind a reference.
References to Trait Objects: &dyn Trait
A trait object reference &dyn Trait consists of a data pointer (pointing to the concrete type’s instance) and a pointer to a vtable — a table of function pointers that maps trait methods to the concrete implementations.
trait Greet {
fn say_hello(&self);
}
struct Person;
impl Greet for Person {
fn say_hello(&self) { println!("Hello, I'm a person."); }
}
let greeter: &dyn Greet = &Person;
greeter.say_hello(); // Dispatch through vtable.
The vtable pointer allows Rust to call the correct method without knowing the concrete type at compile time. This dynamic dispatch is the cost of the fat pointer — one extra word and an indirect function call per method invocation.
When a slice or trait object reference goes out of scope, only the reference itself is popped off the stack. The data it points to has its own lifetime and drop logic, which is governed by the ownership rules of whatever owns the actual data.
Summary
Thinking about references as values — not just transient permissions — clarifies many of Rust’s rules. A reference is a small, copyable number that carries a static guarantee of validity. It behaves like a pointer when you copy or compare it, but the compiler layers on top the requirement that it never dangle, never be null, and never violate aliasing constraints. This is the foundation that makes references safe without runtime overhead.
The two most practical takeaways are:
- References implement
Copy, so assignment duplicates the pointer. This is cheap and safe because the borrow checker tracks all copies together. - Not all references are 8 bytes. Slices and trait objects are fat pointers that bundle extra metadata. The memory representation you see in diagrams matches exactly what the hardware sees.