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.
Why this matters
Without auto-deref and auto-borrow, Rust method calls would be noisy:
(*(*boxed_value)).some_method()
Instead, Rust lets you write:
boxed_value.some_method()
This is especially important for:
- references like
&T - smart pointers like
Box<T>,Rc<T>, andArc<T> - wrapper types that implement
Deref - methods taking
&selfor&mut self, which are very common
Important distinction: method calls vs normal function calls
This behavior is specific to method-call syntax.
value.method()
Rust is more flexible here than with regular function calls:
Type::method(value)
In fully qualified or plain function-call style, Rust does not perform the same receiver adaptation in the same ergonomic way.
Receiver forms Rust supports
A method can take one of these common receiver forms:
self— takes ownership of the value&self— borrows immutably&mut self— borrows mutably
Rust tries to make the left side fit one of these receiver types using auto-deref and auto-borrow.
A practical approximation of the rules
For an expression of type T0, Rust roughly considers:
T0- dereference once ->
T1 - dereference again ->
T2 - and so on
For each candidate Ti, it also considers borrowed forms such as:
&Ti&mut Tiwhen allowed
Then it searches for a visible method whose receiver matches one of those candidates.
Where Deref fits in
If a type implements:
impl Deref for MyType {
type Target = TargetType;
}
then method lookup may continue from MyType to TargetType, and then further if TargetType also dereferences.
That is why this works:
use std::ops::Deref;
struct Wrapper(String);
impl Deref for Wrapper {
type Target = String;
fn deref(&self) -> &String {
&self.0
}
}
fn main() {
let w = Wrapper("hello".to_string());
println!("{}", w.len());
}
Wrapper does not define len(), but String does, so Rust follows Deref and finds it.
Why some results feel surprising
The surprising cases usually come from these facts:
- methods with
&selfare very easy to match because Rust can auto-borrow - methods with
selfmay still work through references if dereferencing yields an owned value that can be moved or copied - if the value is behind a reference and moving is not allowed, the call can fail unless the type is
Copy - trait methods add another layer: Rust searches based on receiver candidates and trait implementations available for those candidate types
Design rationale
The design is intentional and ergonomic:
- References should feel lightweight: calling a method on
&Tshould usually work without ceremony. - Smart pointers should act like the values they point to:
Box<T>,Rc<T>, and similar wrappers would be cumbersome otherwise. - Method-call syntax is meant to be convenient while still preserving Rust’s ownership and borrowing rules.
Rust keeps this convenience mostly constrained to method receivers, where the compiler has a clear target to adapt to. That gives ergonomic code without turning all type conversions into implicit behavior.
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 user as &user.
Auto-deref through references
fn main() {
let user = User {
name: "Ana".to_string(),
};
let r = &user;
r.by_ref();
}
r has type &User, but Rust can dereference it to User for method lookup, then borrow again as needed.
Auto-deref through Deref
use std::ops::Deref;
struct Name(String);
impl Deref for Name {
type Target = String;
fn deref(&self) -> &String {
&self.0
}
}
fn main() {
let n = Name("hello".to_string());
println!("{}", n.len());
println!("{}", n.to_uppercase());
}
Name does not define len() or to_uppercase(), but String does, so Rust follows the Deref target.
By-value methods and Copy
#[derive(Copy, Clone)]
struct A;
impl A {
fn consume(self) {
println!("consumed A");
}
}
fn main() {
let a = A;
(&a).consume();
}
This works because A is Copy, so Rust can copy the value after dereferencing.
If A were not Copy, moving it out of &A would not be allowed.
What Rust roughly searches
If the receiver expression has type &&Wrapper, Rust may consider candidates like:
&&Wrapper&WrapperWrapper- deref target of
Wrapper - deref target of that target
And for each relevant candidate, it may also consider:
&candidate&mut candidatewhen mutable access is possible
This is why method calls can feel smarter than ordinary function calls.
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: 42 };
value.refm();
}
Step-by-step
value has type:
&X
Rust now tries to resolve:
value.refm()
1. Start with the receiver expression type
Initial candidate:
&X
2. Consider dereferencing candidates
Built-in dereference of &X gives:
X
Then X implements Deref<Target = i32>, so another candidate is:
i32
3. Consider borrowed forms where needed
For each candidate, Rust may consider borrowed receivers, especially because refm expects &self.
So it can match:
- method on
Xwith receiver&selfby borrowing theXcandidate - method on
i32with receiver&selfby borrowing thei32candidate
4. Pick the first applicable method in lookup order
Rust finds a matching method on X before needing to go further to i32.
So the chosen implementation is:
impl RefM for X
and the output is:
X::refm()
Another example with nested Deref
use std::ops::Deref;
struct Y {
val: i32,
}
impl Deref for Y {
type Target = i32;
fn deref(&self) -> &i32 {
&self.val
}
}
struct Z {
val: Y,
}
impl Deref for Z {
type Target = Y;
fn deref(&self) -> &Y {
&self.val
}
}
trait RefM {
fn refm(&self);
}
impl RefM for i32 {
fn refm(&self) {
println!("i32::refm()");
}
}
fn main() {
let z = Z { val: Y { val: 42 } };
z.refm();
}
Candidate chain
Starting from Z:
ZYviaDerefi32viaDerefagain
Rust does not find refm on Z or Y, but does find it on i32, so the method call succeeds and prints:
i32::refm()
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) -> &str {
&self.app_name
}
}
fn main() {
let config = Arc::new(Config {
app_name: "demo".to_string(),
});
println!("{}", config.app_name());
}
You call the method on Arc<Config> as if it were directly on Config.
Wrapper types for validation or domain modeling
You might define a wrapper around String:
use std::ops::Deref;
struct Username(String);
impl Deref for Username {
type Target = String;
fn deref(&self) -> &String {
&self.0
}
}
Now string methods become available through the wrapper, though in real code you should be deliberate about whether exposing all inner methods is desirable.
Collections and borrowed access
Methods taking &self are common because they avoid moving values. Auto-borrow means everyday code stays clean:
let items = vec![1, 2, 3];
println!("{}", items.len());
len takes &self, but you do not need to write (&items).len().
APIs designed around references
Many library APIs intentionally use &self for read-only access and &mut self for mutation. Auto-borrow lets callers use these APIs naturally while Rust still enforces ownership rules.
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>) {
println!("{}", service.name());
}
Guard clauses and validation
Method lookup often combines with borrowing-heavy code:
struct Request {
token: Option<String>,
}
impl Request {
fn is_authenticated(&self) -> bool {
self.token.is_some()
}
}
fn handle(req: &Request) {
if !req.is_authenticated() {
return;
}
println!("ok");
}
The method is defined on Request, but called on &Request without extra syntax.
Configuration wrappers
Developers sometimes wrap types for stronger domain meaning:
struct Port(u16);
impl Port {
fn is_system(&self) -> bool {
self.0 < 1024
}
}
Auto-borrow makes these wrappers ergonomic.
Error handling and borrowed methods
Error types often expose borrowed inspection methods:
struct AppError {
message: String,
}
impl AppError {
fn message(&self) -> &str {
&self.message
}
}
This avoids moving data while keeping call sites simple.
Practical advice in codebases
- Prefer
&selffor inspection methods. - Use
&mut selffor mutation. - Use
selfonly when consuming the value is part of the design. - Implement
Derefonly when your type genuinely behaves like a transparent pointer or wrapper. - Do not depend on obscure lookup edge cases for API design; keep receiver choices clear.
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 Wrapper {
type Target = String;
fn deref(&self) -> &String {
&self.0
}
}
Do not assume Wrapper can be used anywhere a String is expected by value.
Mistake 3: Trying to move out through a shared reference
Broken example:
struct A;
impl A {
fn consume(self) {}
}
fn main() {
let a = A;
let r = &a;
// r.consume();
}
If A is not Copy, this fails because Rust will not move A out of &A.
How to avoid it
- use
&selfif consuming ownership is unnecessary - clone explicitly when appropriate
- only rely on this working for
Copytypes when that is truly intended
Mistake 4: Overusing Deref for business types
Just because you can expose inner methods does not mean you should.
use std::ops::Deref;
struct Email(String);
impl Deref for Email {
type Target = String;
fn deref(&self) -> &String {
&self.0
}
}
This makes all String methods available on Email, which may weaken your abstraction.
How to avoid it
Implement Deref mainly for pointer-like wrappers or very transparent newtypes.
Mistake 5: Forgetting &mut self needs mutable access
Broken example:
struct Counter(i32);
impl Counter {
fn inc(&mut self) {
self.0 += 1;
}
}
fn main() {
let counter = Counter(0);
// counter.inc();
}
This fails because the value is not mutable.
How to avoid it
Declare the binding as mutable:
fn main() {
let mut counter = Counter(0);
counter.inc();
}
Mistake 6: Expecting infinite lookup freedom
Rust tries sensible receiver candidates, but method resolution is still rule-based. If several trait methods compete or ownership rules block a candidate, the call may fail or require disambiguation.
How to avoid it
Use fully qualified syntax when needed:
TraitName::method(&value)
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 | Auto-deref/auto-borrow ergonomics |
|---|---|---|
| Method call | value.method() | Yes, special receiver lookup applies |
| Associated function / UFCS | Type::method(&value) | More explicit, less automatic feeling |
Built-in dereference vs Deref
| Kind | Example | What it does |
|---|---|---|
| Built-in dereference | &T -> T in lookup | removes reference layers |
Deref trait | Wrapper -> Target | lets wrapper act like target for method lookup and coercions |
Auto-deref vs explicit dereference
| Approach | Example | When to use |
|---|---|---|
| Auto-deref | boxed.len() | normal method calls |
| Explicit dereference | (*ptr).field | field access, clarity, or when required |
Deref vs AsRef
| Trait | Purpose | Best for |
|---|---|---|
Deref | pointer-like behavior | smart pointers and transparent wrappers |
AsRef | cheap reference conversion | API parameters and generic borrowing |
AsRef is often better than Deref when you want controlled conversion behavior without making one type feel exactly like another.
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.len(); // auto-borrow to &self
let r = &s;
r.len(); // auto-deref + auto-borrow
let b = Box::new(String::from("abc"));
b.len(); // auto-deref through Box
Important rules
- Method-call syntax is special.
&selfmethods are easiest to call.selfmethods require ownership or aCopyvalue.Derefparticipates in method lookup.Derefis not a universal implicit conversion.&mut selfrequires mutable access.
Edge cases
- Calling a
selfmethod through&Tfails if moving out is not allowed. - Trait method ambiguity may require fully qualified syntax.
- Implement
Derefonly for pointer-like or transparent wrapper behavior.
Mental shortcut
Think: Rust tries the receiver, dereferences it step by step, and borrows when useful.
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)?
Because Rust gives special receiver adaptation to method calls to improve ergonomics while keeping conversions limited and predictable.
Can Rust keep dereferencing forever?
No. Method lookup follows a finite candidate search based on actual deref steps available from the type.
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
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.