Question

Rust Auto-Dereferencing Rules in Method Calls Explained

rustreferencelanguage-lawyer

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:

  1. Builds candidate receiver types from the expression on the left side of the dot.
  2. 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 (and DerefMut in mutable cases)
  • then, for each candidate type T, Rust also considers borrowed forms like &T and &mut T when 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>, and Arc<T>
  • wrapper types that implement Deref
  • methods taking &self or &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 Ti when 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 &self are very easy to match because Rust can auto-borrow
  • methods with self may 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 &T should 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
  • &&T
  • Box<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.

Take Quiz

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
  • &Wrapper
  • Wrapper
  • deref target of Wrapper
  • deref target of that target

And for each relevant candidate, it may also consider:

  • &candidate
  • &mut candidate when 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 X with receiver &self by borrowing the X candidate
  • method on i32 with receiver &self by borrowing the i32 candidate

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:

  • Z
  • Y via Deref
  • i32 via Deref again

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.

Take Quiz

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 &self for inspection methods.
  • Use &mut self for mutation.
  • Use self only when consuming the value is part of the design.
  • Implement Deref only 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 &self if consuming ownership is unnecessary
  • clone explicitly when appropriate
  • only rely on this working for Copy types 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

ReceiverMeaningMoves value?Common use
selftakes ownershipYesconsuming builders, conversions, finalization
&selfimmutable borrowNoreading data, helpers, queries
&mut selfmutable borrowNoupdating internal state

Method-call syntax vs function-call syntax

StyleExampleAuto-deref/auto-borrow ergonomics
Method callvalue.method()Yes, special receiver lookup applies
Associated function / UFCSType::method(&value)More explicit, less automatic feeling

Built-in dereference vs Deref

KindExampleWhat it does
Built-in dereference&T -> T in lookupremoves reference layers
Deref traitWrapper -> Targetlets wrapper act like target for method lookup and coercions

Auto-deref vs explicit dereference

ApproachExampleWhen to use
Auto-derefboxed.len()normal method calls
Explicit dereference(*ptr).fieldfield access, clarity, or when required

Deref vs AsRef

TraitPurposeBest for
Derefpointer-like behaviorsmart pointers and transparent wrappers
AsRefcheap reference conversionAPI parameters and generic borrowing

AsRef is often better than Deref when you want controlled conversion behavior without making one type feel exactly like another.

Take Quiz

Cheat Sheet

Receiver lookup quick reference

When resolving:

value.method()

Rust roughly:

  1. Starts from the type of value
  2. Repeatedly dereferences it
    • built-in deref for references
    • Deref / DerefMut where applicable
  3. For each candidate type, also considers borrowed forms
    • &T
    • &mut T when allowed
  4. 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.
  • &self methods are easiest to call.
  • self methods require ownership or a Copy value.
  • Deref participates in method lookup.
  • Deref is not a universal implicit conversion.
  • &mut self requires mutable access.

Edge cases

  • Calling a self method through &T fails if moving out is not allowed.
  • Trait method ambiguity may require fully qualified syntax.
  • Implement Deref only 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.

Take Quiz

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 String and implements Deref<Target = String>.
  • Add one method on the wrapper itself and use at least one inherited String method through auto-deref.
  • Add a second example showing a self method and explain why ownership matters.
  • Print outputs that make it clear which methods were called.
Take Quiz

Keep learning

Related questions

View more