Question
I am learning Rust and trying to understand the exact rules Rust uses for automatic dereferencing during method calls.
Rust automatically dereferences values when resolving methods, but the behavior can feel surprising. I experimented with a number of combinations involving:
- methods taking
self - methods taking
&self - multiple layers of references like
&X,&&X, and&&&X - user-defined dereferencing via
Deref
Here is the test program:
#[derive(Copy, Clone)]
struct X {
val: i32,
}
impl std::ops::Deref for X {
type Target = i32;
fn deref(&self) -> &i32 {
&self.val
}
}
trait M {
fn m(self);
}
impl M for i32 {
fn m(self) {
println!("i32::m()");
}
}
impl M for X {
fn m(self) {
println!("X::m()");
}
}
impl M for &X {
fn m(self) {
println!("&X::m()");
}
}
impl M for &&X {
fn m(self) {
println!("&&X::m()");
}
}
impl M for &&&X {
fn m(self) {
println!("&&&X::m()");
}
}
trait RefM {
fn refm(&self);
}
impl RefM for i32 {
fn refm(&self) {
println!("i32::refm()");
}
}
impl RefM for X {
fn refm(&self) {
println!("X::refm()");
}
}
impl RefM for &X {
fn refm(&self) {
println!("&X::refm()");
}
}
impl RefM for &&X {
fn refm(&self) {
println!("&&X::refm()");
}
}
impl RefM for &&&X {
fn refm(&self) {
println!("&&&X::refm()");
}
}
struct Y {
val: i32,
}
impl std::ops::Deref for Y {
type Target = i32;
fn deref(&self) -> &i32 {
&self.val
}
}
struct Z {
val: Y,
}
impl std::ops::Deref for Z {
type Target = Y;
fn deref(&self) -> &Y {
&self.val
}
}
#[derive(Clone, Copy)]
struct A;
impl M for A {
fn m(self) {
println!("A::m()");
}
}
impl M for &&&A {
fn m(self) {
println!("&&&A::m()");
}
}
impl RefM for A {
fn refm(&self) {
println!("A::refm()");
}
}
impl RefM for &&&A {
fn refm(&self) {
println!("&&&A::refm()");
}
}
fn main() {
(*X { val: 42 }).m();
X { val: 42 }.m();
(&X { val: 42 }).m();
(&&X { val: 42 }).m();
(&&&X { val: 42 }).m();
(&&&&X { val: 42 }).m();
(&&&&&X { val: 42 }).m();
println!("-------------------------");
(*X { val: 42 }).refm();
X { val: 42 }.refm();
(&X { val: 42 }).refm();
(&&X { val: 42 }).refm();
(&&&X { val: 42 }).refm();
(&&&&X { val: 42 }).refm();
(&&&&&X { val: 42 }).refm();
println!("-------------------------");
Y { val: 42 }.refm();
Z { val: Y { val: 42 } }.refm();
println!("-------------------------");
A.m();
(&A).m();
(&&A).m();
(&&&A).m();
A.refm();
(&A).refm();
(&&A).refm();
(&&&A).refm();
}
From these experiments, it looks like Rust inserts dereferences and borrows during method resolution until it finds a matching receiver type, and that Deref also participates in the search.
What are Rust’s exact auto-dereferencing and auto-borrowing rules for method calls? Also, what is the rationale behind this design?
Short Answer
By the end of this page, you will understand how Rust resolves value.method() calls by building candidate receiver types, applying repeated dereferencing, and then trying automatic borrowing with & and &mut where appropriate. You will also see how Deref affects method lookup, why methods with self, &self, and &mut self behave differently, and how these rules make smart pointers and references ergonomic in real Rust code.
Concept
Rust method calls use more than simple dot syntax. When you write:
value.method()
Rust does method receiver lookup. It does not only check the exact type of value. Instead, it tries a series of possible receiver forms until it finds a method that matches.
The core idea
For a method call, Rust conceptually does two things:
- Builds candidate receiver types from the expression on the left side of the dot.
- Searches for a matching method on those candidates.
The candidate list is built by repeatedly applying:
- built-in dereferencing of references like
&T -> T - user-defined dereferencing through
Deref(andDerefMutin mutable cases) - then, for each candidate type
T, Rust also considers borrowed forms like&Tand&mut Twhen valid
That is why a method defined on T may be callable on &T, Box<T>, Rc<T>, or your own smart-pointer-like type if Deref is implemented.
Mental Model
Think of Rust method lookup like trying keys on a ring.
You start with the exact value on the left of the dot.
If that key does not open the door, Rust tries nearby variations:
- the value itself
- one dereference away
- two dereferences away
- borrowed versions like
&value - mutable borrowed versions when allowed
It keeps trying sensible receiver shapes until it finds a method that fits.
Analogy
Imagine you have a package inside several layers:
T&T&&TBox<T>- custom wrapper types using
Deref
Rust method lookup is like peeling layers or temporarily holding the package differently until it can hand it to the method in the form the method expects.
- If the method wants to look at the package, Rust can often borrow it as
&self. - If the method wants to change the package, Rust may borrow it as
&mut self. - If the method wants to take the package, Rust must be able to move or copy the value.
That last point explains many edge cases: borrowing is easy, but moving through a reference is restricted.
Syntax and Examples
Basic receiver forms
struct User {
name: String,
}
impl User {
fn by_value(self) {
println!("owned: {}", self.name);
}
fn by_ref(&self) {
println!("borrowed: {}", self.name);
}
fn by_mut_ref(&mut self) {
self.name.push('!');
}
}
Auto-borrow with &self
fn main() {
let user = User {
name: "Ana".to_string(),
};
user.by_ref();
}
Even though by_ref expects &self, Rust automatically borrows as .
Step by Step Execution
Consider this example:
use std::ops::Deref;
struct X {
val: i32,
}
impl Deref for X {
type Target = i32;
fn deref(&self) -> &i32 {
&self.val
}
}
trait RefM {
fn refm(&self);
}
impl RefM for i32 {
fn refm(&self) {
println!("i32::refm()");
}
}
impl RefM for X {
fn refm(&self) {
println!("X::refm()");
}
}
fn main() {
let value = &X { val: };
value.();
}
Real World Use Cases
Calling methods on smart pointers
Rust code frequently uses pointer-like types:
Box<T>Rc<T>Arc<T>String- custom wrappers implementing
Deref
Auto-deref lets you call inner methods naturally:
let name = Box::new(String::from("rust"));
println!("{}", name.len());
Working with shared application state
In web servers or async applications, values are often wrapped in Arc<T>.
use std::sync::Arc;
struct Config {
app_name: String,
}
impl Config {
fn app_name(&self) -> & {
&.app_name
}
}
() {
= Arc::(Config {
app_name: .(),
});
(, config.());
}
Real Codebase Usage
Common pattern: mostly &self methods
In real Rust projects, many methods are defined with &self because:
- they do not consume the value
- they are easier to call
- they work well with references and smart pointers
struct Parser {
source: String,
}
impl Parser {
fn len(&self) -> usize {
self.source.len()
}
}
Smart-pointer ergonomics
Libraries rely on Deref so wrapped values remain easy to use.
use std::rc::Rc;
struct Service {
name: String,
}
impl Service {
fn name(&self) -> &str {
&self.name
}
}
fn print_service(service: Rc<Service>) {
(, service.());
}
Common Mistakes
Mistake 1: Assuming method-call auto-deref applies everywhere
Method syntax is special.
struct User;
impl User {
fn greet(&self) {}
}
fn main() {
let user = &User;
user.greet(); // works
// User::greet(user); // this is different style and may not feel as flexible
}
How to avoid it
Remember that value.method() has receiver lookup rules that are more ergonomic than ordinary function calls.
Mistake 2: Thinking Deref means “implicit conversion everywhere”
Deref mainly helps with:
- method calls
- deref coercions in some reference contexts
It is not a universal automatic conversion mechanism.
use std::ops::Deref;
struct Wrapper(String);
impl Deref for {
= ;
(&) & {
&.
}
}
Comparisons
self vs &self vs &mut self
| Receiver | Meaning | Moves value? | Common use |
|---|---|---|---|
self | takes ownership | Yes | consuming builders, conversions, finalization |
&self | immutable borrow | No | reading data, helpers, queries |
&mut self | mutable borrow | No | updating internal state |
Method-call syntax vs function-call syntax
| Style | Example |
|---|
Cheat Sheet
Receiver lookup quick reference
When resolving:
value.method()
Rust roughly:
- Starts from the type of
value - Repeatedly dereferences it
- built-in deref for references
Deref/DerefMutwhere applicable
- For each candidate type, also considers borrowed forms
&T&mut Twhen allowed
- Chooses a matching visible method
Common receiver forms
fn a(self)
fn b(&self)
fn c(&mut self)
What usually works
let s = String::from("abc");
s.();
= &s;
r.();
= ::(::());
b.();
FAQ
Why can I call a &self method on a value without writing &?
Because Rust automatically borrows the receiver in method-call syntax when the method expects &self.
Why does calling a self method on &T sometimes fail?
Because self means ownership. Rust cannot move a non-Copy value out of a shared reference.
Does Deref apply only to method calls?
No. It also affects deref coercions in some reference contexts, but method calls are where it is most visible.
Why does a wrapper type gain methods from its inner type?
If the wrapper implements Deref<Target = Inner>, Rust can continue method lookup on Inner.
Is auto-deref the same as implicit type conversion?
No. It is a specific receiver-lookup and coercion mechanism, not general implicit conversion.
When should I implement Deref?
Usually for smart pointers or very transparent wrappers. Avoid it for arbitrary business-domain conversions.
Why is method-call syntax more flexible than Type::method(value)?
Mini Project
Description
Build a small Rust program that demonstrates how method lookup behaves across plain values, references, and custom wrapper types that implement Deref. This helps make the auto-deref rules concrete by showing which methods get called and why.
Goal
Create a runnable example where a wrapper type can call methods defined on itself and on its deref target, and compare behavior for self and &self receivers.
Requirements
- Create a struct that wraps a
Stringand implementsDeref<Target = String>. - Add one method on the wrapper itself and use at least one inherited
Stringmethod through auto-deref. - Add a second example showing a
selfmethod and explain why ownership matters. - Print outputs that make it clear which methods were called.
Keep learning
Related questions
Accessing Cargo Package Metadata in Rust
Learn how to read Cargo package metadata like version, name, and authors in Rust using compile-time environment macros.
Default Function Arguments in Rust: What to Use Instead
Learn how Rust handles default function arguments, why they are not supported, and practical patterns to achieve similar behavior.
Fixing Rust "linker 'cc' not found" on Debian in WSL
Learn why Rust shows "linker 'cc' not found" on Debian in WSL and how to fix it by installing the required C build tools.