Method call resolution behaviour

The following code outputs in inherent impl, while I am expecting in trait impl. To my understanding, per the Method Call Expression Reference, the candidate list should be &Foo, &&Foo, &mut &Foo, Foo, &Foo, &mut Foo, so the trait method should be found first, but not.

I found What are Rust's exact auto-dereferencing rules?, the question mentioned

The compiler, when resolving methods declared using &self (call-by-reference):

  • First tries calling for a single dereference of self

but no answer clearly talked about this. Could somebody shed more light on this behavior ?

trait Trait {
    fn method(self);
}

struct Foo;

impl Foo {
    fn method(&self) {
        println!("in inherent impl");
    }
}

impl Trait for &Foo {
    fn method(self) {
        println!("in trait impl");
    }
}

fn main() {
    let foo = &Foo;
    foo.method();
}

By the way, I fire an issue about this on the rust reference repository

Indeed, the resolution attempts to use methods using no .-indirection, and then starts adding & and * operations as necessary to see if it finds a candidate, as mentioned in the reference you linked.

That's one rule, the other one is that when a trait method and an inherent method compete at the same level, the inherent method takes precedence over the trait method.


Finally, when all the available methods at a given level of indirection come from (different) trait methods (no inherent impls), then, when multiple of these traits are in scope, a method resolution ambiguity error occurs: Playground

2 Likes

Hi, @Yandros , thanks for your reply. I still don't quit get your point. Let's look at the deref steps,

  1. &Foo
  2. Foo

so, these are you mean by a given level of inderection, right? The compiler then looks at &Foo, it finds Trait::method. &Foo is not a nomial type, it has no impl block and thus no inherent methods. So the compiler is done with no competing methods ?

That is incorrect. The inherent Foo::method takes &self and so it can be called on &Foo.

1 Like

True, I meant "inherent method" in the broader sense that impl Foo { fn method(&self, … can be seen as an inherent method, à la impl &Foo { fn method(self, …. w.r.t. method resolution they do work like that.

Round one, find candidates by dereferencing (and maybe an unsized coercion):

  • &Foo
  • Foo

Round two, add &T and &mut T after each T:

  • &Foo
  • &&Foo
  • &mut &Foo
  • Foo
  • &Foo
  • &mut Foo

Round three, find methods for the first of these with a receiver that matches:

  • &Foo is the first and we find two receivers:
    • Foo::method receives &Foo
    • Trait::method receives &Foo

The first is an inherent method and it wins.

6 Likes

@Yandros @quinedot thanks for your explanations! So actually, although the candidate type is &Foo, the compiler will look methods of Foo also.

Yes, what matters in not the Self type of the impl, but the type of the (method's) receiver. This is more explicit if you ditch the [&]self shorthand sugar:

impl Foo {
    fn method (self: &'_ Foo, …) // fn(&self, …)
}

impl Trait for &'_ Foo {
    fn method (self: &'_ Foo, …) // fn(self, …)
}

As you can see, the only way to know whether it originally is a self, or &self, method is by forcing ourselves into looking what Self looks like at the impl line.

2 Likes

Found something interesting, the standard library has a blanket implementation of Clone for immutable references since they are Copy,

impl<T: ?Sized> Clone for &T {
        fn clone(&self) -> Self {
            *self
        }
 }

but there are many types also implement Clone, like Rc, Arc, Vec etc. And we would expect the following code to clone the objects instead of a reference.

let data = vec![0; 10];
let data = &data;
let clone = data.clone(); // clone the data, not the reference!

In addition to the above, Arc has a section in the documentation that covers the behaviour of calling clone() on Arc itself vs Deref-ed value:

Arc<T> ’s implementations of traits like Clone may also be called using fully qualified syntax. Some people prefer to use fully qualified syntax, while others prefer using method-call syntax.

use std::sync::Arc;

let arc = Arc::new(());
// Method-call syntax
let arc2 = arc.clone();
// Fully qualified syntax
let arc3 = Arc::clone(&arc);

See std::sync::Arc - Rust

That code does clone the Vec, and not the reference to a Vec, because of the method call resolution rules spelled in this very thread:

  • Clone::clone(data) is directly usable, i.e., <Vec<_> as Clone>::clone(data);

  • In order to "Clone" (copy) the reference, we'd need to run <&_ as Clone>::clone, which thus expects a &&_ parameter, which data is not.

Code that accidentally copies the reference rather than cloning the Vec would be like:

let data = vec![0; 10];
let data = &&data;
let clone = data.clone(); // copies the reference

or

let data = vec![0; 10];
let data = &data;
let clone = (&data).clone(); // copies the reference

Neither of which feels natural to write.

I guess a more realistic approach would be:

let vec1 = vec![…];
let vec2 = vec![…];
for vec in [&vec1, &vec2].iter() {
    let cloned_vec = vec.clone(); // Whoops, copied a reference!
}
  • or worse, before the 2021 edition, the iteration may even be written as for vec in [&vec1, &vec2].into_iter() {

That being said, this is not a real problem, since:

  • the moment you intend to use cloned_vec where a Vec is expected:

    • if it matters, you'll get a type error complaining that the type of cloned_vec is &'_ Vec and not Vec;

    • if it doesn't matter, you won't get a type error: this means that clone-ing the Vec, in and of itself, was unnecessary to begin with!

  • The worst that could happen is a less readable error message about things not being 'static or things like that:

    let vec1 = vec![42; 42];
    let vec2 = vec![42; 42];
    let mut thread_handles = Vec::with_capacity(2);
    for vec in [&vec1, &vec2].iter() {
        let cloned_vec = vec.clone(); // Whoops, copied a reference!
        thread_handles.push(::std::thread::spawn(move || {
            /* stuff with `cloned_vec` */
            let _ = cloned_vec;
        }));
    }
    

    which will error with captured `&Vec` is not 'static. Which may not necessarily be as readable as expected `Vec`, got `&Vec`., but is ultimately the same kind of type error.

1 Like