Reference Operators in Rust Expressions
Learn how the &, &mut, and * operators work as expressions that create and dereference references, and how they connect to borrowing, ownership, and operator overloading.
Rust’s reference operators—&, &mut, and *—are expressions that either borrow a value or follow a reference back to the value it points to. They are the syntactic bridge between ownership and borrowing, and they appear constantly in real Rust code. This page explains what each operator does, why they exist, how they work in expressions, and what mistakes trip up beginners.
What Are Reference Operators?
A reference is a value that points to a memory location owned by some other value. Rust has two kinds of references:
- Immutable (shared) references:
&T - Mutable (exclusive) references:
&mut T
The operators that create these references are & and &mut. The operator that follows a reference to the value it points to is *. All three are expressions: they evaluate to a result that you can use in assignments, function calls, or further computation.
These operators exist because Rust’s ownership system requires explicit tracking of who can read or write data. Without them, the compiler could not enforce the rule that you may have one mutable reference or many immutable references at any time, which eliminates data races at compile time.
Creating References with & and &mut
When you write &value, you create an immutable reference to value. The result is a value of type &T where T is the type of the original value. The original value remains in place; it is borrowed, not moved.
let x = 42;
let r: &i32 = &x; // r is a reference to x
println!("{}", r); // prints 42
The same idea works for &mut, which produces an exclusive, mutable reference. While a mutable reference exists, no other reference (mutable or immutable) to the same data can exist.
let mut y = 10;
let r_mut: &mut i32 = &mut y;
*r_mut += 5;
println!("{}", y); // prints 15
Borrowing as a permission:
Think of & as asking for read-only access to a value. You can have as many readers as you want at the same time. &mut asks for exclusive write access; nobody else can read or write while you hold the write permission.
The expressions &x and &mut y do not consume the underlying value. The variable x or y is still usable after the reference is created, provided the borrow checker’s rules are satisfied. This is a key difference from moving a value, where the original variable would become invalid.
A beginner mental model: references are like mailing addresses. An address tells you where someone lives, but it isn’t the person itself. & creates an address, and * (covered next) goes to that address to interact with the person.
Dereferencing with *
The * operator follows a reference to access the underlying value. If you have a reference r: &T, then *r gives you a place expression that refers to the original value—you can read it or, if r is a &mut T, write to it.
let mut a = 7;
let b = &mut a;
*b = 3; // writes through the mutable reference
assert_eq!(*b, 3);
Dereferencing an immutable reference yields a read-only view of the value:
let v = vec![1, 2, 3];
let first_ref = &v[0];
println!("First element is {}", *first_ref);
Moving out of a reference:
You cannot move a value out of a reference unless the type implements Copy. For non-Copy types, attempting to assign *r to a variable without cloning will fail, because that would leave the original location empty while a reference still points to it.
let s = String::from("hello");
let r = &s;
// let moved = *r; // ERROR: cannot move out of dereference of `&String`
If you need to take ownership, clone the value explicitly: let cloned = r.clone();
In many situations Rust automatically dereferences for you—for example, when you call a method on a reference. This is called auto-deref. However, * is still needed when you want to assign through a mutable reference or when you need the actual value for something like pattern matching or explicit copying.
Reference Operators in Expressions
Since &, &mut, and * are expressions, they can appear anywhere an expression is expected. This enables concise code that borrows values inline.
fn is_positive(x: &i32) -> bool {
*x > 0
}
let num = -5;
// Borrowing and dereferencing in a single line
let result = is_positive(&num);
You can chain references, though that’s rarely intentional:
let x = 10;
let r = &x; // &i32
let rr = &r; // &&i32
println!("{}", **rr); // double dereference to get 10
Reference operators also interact with the ? operator and closures. For instance, you can create a reference to a temporary value inside a closure, but you must ensure the temporary lives long enough—the borrow checker enforces this.
Auto-Referencing and Auto-Dereferencing
Rust does not automatically insert & for you in most expressions. If a function expects a &T and you pass a T, the compiler will not silently borrow it—you must write &value explicitly. The same holds for arithmetic operators: a + b works on the values themselves, not on references. There is no auto-ref for operators.
This design choice surfaces clearly when you overload operators. Consider this example:
struct Point { x: i32, y: i32 }
impl std::ops::Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point { x: self.x + other.x, y: self.y + other.y }
}
}
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let p3 = p1 + p2; // works, because we defined Add for Point
If you instead defined impl Add<&Point> for Point or for &Point, then p1 + p2 would fail unless you wrote &p1 + &p2. Many libraries provide implementations for both owned values and references to keep calling code ergonomic, but you must be aware of what’s implemented.
Operators do not auto-ref:
There is no mechanism that tries &a + &b if a + b fails. If you see an error like “cannot add &T to &T”, you likely forgot to implement the operator for the reference types, or you need to manually add & at the call site.
Auto-dereferencing does happen in method calls and field access. When you write my_ref.method(), the compiler will try (*my_ref).method(), (**my_ref).method(), and so on until it finds a matching method. This is why you can call .len() on a &Vec<T> without writing * first. This convenience does not extend to operators—it’s specific to the . syntax.
Reference Operators and Operator Overloading
When you implement traits like std::ops::Add, Sub, Mul, etc., you decide whether the operator works on owned values, references, or both. If you provide only a reference-based implementation, you force callers to sprinkle & everywhere.
A common pattern is to define a blanket implementation that forwards from owned values to references, so that both a + b and &a + &b work. For example:
use std::ops::Add;
#[derive(Debug, Copy, Clone)]
struct Vec2 { x: f32, y: f32 }
// Base implementation for references
impl<'a, 'b> Add<&'b Vec2> for &'a Vec2 {
type Output = Vec2;
fn add(self, rhs: &'b Vec2) -> Vec2 {
Vec2 { x: self.x + rhs.x, y: self.y + rhs.y }
}
}
// Forward for owned values by borrowing
impl Add<Vec2> for Vec2 {
type Output = Vec2;
fn add(self, rhs: Vec2) -> Vec2 {
&self + &rhs
}
}
With both implementations, users can write v1 + v2 without cluttering the expression with references. The underlying machinery uses reference operators, but they stay hidden from the caller.
Ergonomic operator overloading:
If you implement operators on references and then add forwarding impls for owned types, your types work naturally in expressions. The compiler will reuse the reference impl and the call sites stay clean.
Common Mistakes and Misconceptions
Thinking & creates a copy
A reference does not duplicate the data; it merely creates a pointer. However, if the type is Copy, dereferencing and moving out of a reference will copy the bits, which might give the illusion that the reference “copied” the value.
let x = 10;
let r = &x;
let y = *r; // Copies the i32, x is still valid
Trying to mutate through an immutable reference
This is a frequent compiler error: you wrote &self in a method signature but then tried to change a field. The fix is to use &mut self.
Holding a reference while modifying the original value
Once you create a reference, the borrow checker prevents you from modifying the original value until the reference is no longer used. This includes moving the original value.
let mut s = String::from("hello");
let r = &s;
// s.push_str(" world"); // ERROR: cannot borrow `s` as mutable because it is also borrowed as immutable
println!("{}", r);
Confusion around * and auto-deref
Newcomers often overuse * because they think they must manually dereference before calling a method. Rust’s auto-deref means you almost never need (*ref).method(). Use ref.method() directly. Reserve explicit * for assignments through mutable references or when you need the value itself for a non-method operation.
Expecting auto-ref in arithmetic
When working with numeric types from external crates (like num::BigInt), you might see a + &b where b is a reference. This is because the library defines impl Add<&BigInt> for BigInt but not Add<BigInt> for BigInt. To avoid surprises, check the trait implementations.
Summary
Reference operators are the syntactic glue that lets Rust’s borrow checker enforce memory safety without a garbage collector. & and &mut create borrows, and * follows them back to the data. They are ordinary expressions, so they integrate seamlessly into assignments, function arguments, and arithmetic—but they do not appear automatically. Understanding when you need them, and when Rust inserts dereferences on your behalf, removes a major source of beginner confusion.
One key takeaway: if operator expressions look ugly with many &s, the problem is not the language—it’s that the type’s operator overloads haven’t been defined for owned values. Writing forwarding impls cleans up the syntax without sacrificing safety.