When will auto-deref happen for receiver in method call?

Hello!

I'm yet again baffled in my journey of Rust.

Consider this code snippet:

trait Barkable {
    fn bark(&self);
}

struct Dog{}

impl Barkable for Dog {
    fn bark(&self) {
        println!("woof!");
    }
}

impl Barkable for &Dog {
    fn bark(&self) {
        println!("borrowed woof!");
    }
}

fn main() {
    let d = Dog{};
    
    let rd = &d;
    rd.bark(); // woof!
    
    let rrd = &&d;
    rrd.bark(); // borrowed woof!
}

When we write rd.bark(), from the actual output it seems that rd is automatically dereferenced. However I don't quite understand why auto-deref is necessary here, since the trait is also implemented on &Dog, not just Dog.

Note that after a second layer of reference is added, Rust did what I expect, by removing one layer of reference to find the implementation.

I'm still confused after carefully reading the chapter expressions/method-call-expr.html.

Thanks in advance!

1 Like

This is because bark takes &self. Try taking self and you will see the difference.

1 Like

In Rust . operator borrows/dereferences implicitly and will follow as many references as it takes to find its target.
Same behavior happens with comparison operators... :wink:

1 Like

Thanks for the hint, and your suggestion indeed produced the expected behaviour. But I still don't quite understand: writing &self in the signature just means the concrete receiver will be borrowed. However I'm not able to see the relevance to my question.

Thanks! However in my example, there is no need to dereference in order to find the "target", the trait is already implemented on the reference.

I don't see where dereferencing happens in your example. The "woof" function receives a &Dog, so when you call bark() on a &Dog, it's called. The "borrowed woof" function receives a &&Dog, so when you call bark() on a &&Dog, it's called.

To better understand the type of self argument, you can write it explicitly (although I would not recommend doing that in real code). The following is equivalent to your example:

impl Barkable for Dog {
    fn bark(self: &Dog) {
        println!("woof!");
    }
}

impl<'a> Barkable for &'a Dog {
    fn bark(self: &&'a Dog) {
        println!("borrowed woof!");
    }
}
3 Likes

Thanks! Now I see the core of my confusion: What is the "method set" of a given type? (Borrowing the Go term here)

According to your and @sanxiyn's post, if I write:

impl Barkable for &Dog {
    fn bark(self: &Self) {}
}

...this particular instance of method bark() actually belongs to the method set of &&Dog, which is determined by the parameter in method signature, not &Dog, which is in the impl header.

However, I don't think the documentation is clear on this.

The documentation says:

It's unclear to me what is the "type of receiver" here. Is it the parameter (expected type)? Or the argument (actual type)?

You may find it helpful to have the rules from that part of the reference worked through in this case (although I don't know if that would be too much detail). First, note that what is important is the receiver (self) types for the two bark() implementations, which are &Dog for "woof" and &&Dog for "borrowed woof". It is these receiver types that we are matching against.

First, we make a list of candidate receiver types to search for. In the first method call, the receiver expression rd has type &Dog. Following the rules for method call expressions that you linked, we try taking it as-is first, so &Dog is our first candidate type, then we try dereferencing, so Dog is our second candidate type. We can then try an unsized coercion after that, so we would add dyn Barkable to the list. Then we add borrowed forms to the list after each of the candidate types, so our list is now:

  • &Dog
  • &&Dog
  • &mut &Dog
  • Dog
  • &Dog
  • &mut Dog
  • dyn Barkable
  • &dyn Barkable
  • &mut dyn Barkable

Then we go down this list, searching for an appropriate method with that receiver type. First up is &Dog and we check for things implemented directly on &Dog first, that is, non-trait methods. There aren't any non-trait methods for &Dog, so we proceed to trait methods. You have two trait methods there, one taking a self parameter of type &Dog and one taking a self parameter of type &&Dog. Since &Dog matches the first one, that one is taken, and it prints "woof".

For the second method call, rrd is of type &&Dog so we get candidate types &&Dog, &Dog, Dog and dyn Barkable. Expanding the list with borrowed forms as before yields:

  • &&Dog
  • &&&Dog
  • &mut &&Dog
  • &Dog
  • &&Dog
  • &mut &Dog
  • Dog
  • &Dog
  • &mut Dog
  • dyn Barkable
  • &dyn Barkable
  • &mut dyn Barkable

The first type we then consider is &&Dog. As before, there are no non-trait methods. There are still trait methods for &Dog and &&Dog, but this time we match the &&Dog implementation, which prints "borrowed woof".

2 Likes

It's a good question. The docs are indeed unclear on this. Type of receiver is the type of the self argument. See Method Syntax page of the book:

This automatic referencing behavior works because methods have a clear receiver—the type of self . Given the receiver and name of a method, Rust can figure out definitively whether the method is reading ( &self ), mutating ( &mut self ), or consuming ( self ).

2 Likes

Unsized coercions are not part of the autoderef rules, so dyn Barkable should not be on this list. You can't call a method defined for dyn Barkable on a value of Dog unless you first cast it to dyn Barkable.

I always refer to this StackOverflow answer by @huon when the autoderef algorithm comes up, so I'll copy a part of it here:

The core of the algorithm is:

  • For each "dereference step" U (that is, set U = T and then U = *T, ...)
    1. if there's a method bar where the receiver type (the type of self in the method) matches U exactly , use it (a "by value method")
    2. otherwise, add one auto-ref (take & or &mut of the receiver), and, if some method's receiver matches &U, use it (an "autorefd method")

Notably, everything considers the "receiver type" of the method, not the Self type of the trait, i.e. impl ... for Foo { fn method(&self) {} } thinks about &Foo when matching the method, and fn method2(&mut self) would think about &mut Foo when matching.

2 Likes

Thank you! This level of detail is completely helpful to me, and I'm sure it will help any other newbie viewing this thread immensely.
So the key point boils down to these:

  1. The candidate types is based on the concrete receiver type. e.g. in f.bar(), it's the type of f;
  2. When searching for a method within a candidate type, whether a method "belongs" to a candidate type depends on the type of self parameter in the relevant signatures, not the type in impl header.

Thanks. Combining your post with @jameseb7's super detailed analysis, I'm able to come to this conclusion too.

However, I couldn't help but notice a subtle weirdness here. When we write

impl Barkable for Dog {
    fn bark(self: &Self)
}

then type Dog implements Barkable. However, despite that, the "method set" of type Dog does not actually have a method bark()! Then what's the point of saying we implemented Barkable for Dog?

The type Dog implements Barkable purely because we wrote the impl header that way, not because we added the relevant method(s) to its method set.

The point is that traits aren't just about methods that take Self by value. They also include other types of API, like methods that take Self by reference or by Box<Self>, etc. They even include things like associated constants and associated functions, which aren't methods at all! (And, importantly, a trait can include a mix of all of these things.)

A trait is not a “method set.” Instead, it a contract that a type can fulfill.

2 Likes

Thanks a lot for the bigger picture! It certainly helped me a lot to form a complete mental model of the trait system.

I agree with that, however I think the concept of "method set" is not totally incompatible with Rust's trait system. For example, by fulfilling the contract of Barkable for Dog, a Dog has to have the behaviour of being able to bark(). But Dog cannot actually bark(). I think this is almost a pure conceptual issue. Of course we can call bark() on a value of Dog, but not because Dog fulfills the contract.

Now, I do realize that expressing ideas like this is probably not something I should do as a near total beginner since I'm still in a phase of gradually familiarizing myself with a lot of new trivia. For example, the above discussed subject also leads to another "weird" example:

let x: i32 = 5;
let rx = &x;
let cloned = rx.clone(); // type of cloned is i32

Is it normal to expect cloned to be a &i32? When I write rx.clone() I certainly want to clone rx, which is the reference. Had I wanted to clone the i32 I would probably write (*rx).clone().

Now, to actually clone the reference, I'll have to write (&rx).clone() or Clone::clone(&rx), which feels like jumping hoops, or trying to defeat the compiler which is being "too clever".

1 Like

If by method set you mean "the set of methods that can be called on an object with . syntax", then trivially, Dog can bark().

On the other hand, if by method set you mean "the set of methods that take Dog as a receiver type", then I guess you technically are correct, but bear in mind that this definition of "method set" does not really exist in the language. It's merely a rhetorical device that we use to define the order in which the . operator resolves methods, not a concept that has inherent meaning outside of that context. It certainly doesn't map to anything like a trait.

1 Like

I was following the rules at the reference link, where it states:

The first step is to build a list of candidate receiver types. Obtain these by repeatedly dereferencing the receiver expression's type, adding each type encountered to the list, then finally attempting an unsized coercion at the end, and adding the result type if that is successful. Then, for each candidate T , add &T and &mut T to the list immediately after T .

You do seem to be correct that it doesn't perform an unsized coercion for trait objects, so the reference may be wrong on this. It seems like it may be worth opening an issue to get the reference corrected or clarified. It looks like it was discussed when that paragraph was written, and mentioned that the unsized coercion only happens for slices, but it doesn't seem to have been corrected at that point as far as I can tell.

EDIT: I went ahead and submitted an issue.

3 Likes

Well, the trait Barkable defines the method bark(&self). This means that the contract requires this method to be implemented for &Dog. Indeed, you can write it in full notation, Barkable::bark(rd), and then it will call the implementation of bark(self: &Dog), which is guaranteed to exist by virtue of Dog implementing Barkable and therefore resides inside that impl. If anything, it is d.bark() which is magical in that it first takes a reference to d to suit the method.

Here again, the same thing happens: the impl of the Clone trait for i32 requires implementing the method clone(&i32) (which is the whole point of clone(), generating an owned value from a reference). When you write x.clone(), magic happens to take a reference to x and pass it to clone(&i32); when you give a reference directly, this isn't necessary.

This also makes sense. Think of such a method:

fn my_func(x: &Foo) -> Bar {
    let x = x.clone();
    // do things with x, to get a Bar
}

What are you probably trying to do? Clone and get an owned Foo, or clone a reference?

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.