Fully qualified syntax and implicit &mut reborrowing

Hi!

I've seen this in a comment in StackOverflow, and can't seem to understand it...

I was under the impression that a simple trait like this:

trait IntoFoo {
    fn into_foo(self);
}

could always be called either like this:

    v.into_foo();

or like this (Fully Qualified Syntax - FQS):

    IntoFoo::into_foo(v);

But, if we happen to implement it for a &mut C:

struct C;

impl<'a> IntoFoo for &'a mut C {
    fn into_foo(self) {
        println!("IntoFoo::into_foo for &mut C");
    }
}

Well, the first form can be called more than once, but why the second can't??

fn do_it(v: &mut C) {
    v.into_foo();
    v.into_foo();
    IntoFoo::into_foo(v);
//    IntoFoo::into_foo(v);
}

fn main() {
    let mut c = C;
    do_it(&mut c);
}

Why does only the fully qualified syntax move the value?
Shouldn't they be synonyms, thus having the same effects?

error[E0382]: use of moved value: `v`
  --> src/main.rs:17:23
   |
13 | fn do_it(v: &mut C) {
   |          - move occurs because `v` has type `&mut C`, which does not implement the `Copy` trait
...
16 |     IntoFoo::into_foo(v);
   |     -------------------- `v` moved due to this method call
17 |     IntoFoo::into_foo(v);
   |                       ^ value used here after move
   |
note: this function takes ownership of the receiver `self`, which moves `v`
  --> src/main.rs:2:17
   |
2  |     fn into_foo(self);
   |                 ^^^^

Playground: Rust Playground

Thank you!

The compiler will do a reborrow with the first form but can't with the second. To do a reborrow manually

fn do_it(v: &mut C) {
    v.into_foo();
    v.into_foo();
    IntoFoo::into_foo(&mut *v);
    IntoFoo::into_foo(v);
}

If you're using Rust analyzer (and you probably should). There's an option called "Reborrow Hints" that will show where the compiler inserts reborrows. The lifetime elision hints can be helpful to see how the compiler adds lifetimes (not related to this question).

2 Likes

The rules for method calls are documented; the somewhat simplified TL;DR is that the compiler tries really hard to find a matching method, and references/dereferences as much as needed. (The actual rules are a bit more nuanced.)

In contrast, UFCS doesn't do any of that, it's just like a regular function call without a special self parameter.

2 Likes

Thank you @Cocalus and @H2CO3!

So, in the first step of the method call syntax, when the compiler builds a list of candidate receiver types, it will dereference that &mut C into a C, then add both &C and &mut C... I think I got it!
And then, when it does find a perfect receiver match in &mut C, it in fact reborrows it, i.e. it in fact dereferenced it to build the list, right?
Very cool!!

Well, when using the FQS the compiler disables all this mechanism and calls it directly, ok till here. But the error still doesn't make sense to me. Actually I never really understood this: a self receiver, but you actually implement that for a &mut.....
The error states "this function takes ownership of the receiver self", granted it is written self, but the actual receiver is still &mut self, isn't it?

v.into_foo(), v still live in do_it,
but when you call IntoFoo::into_foo(v), the v will move to into_foo ?

I am trying to follow the rules on that page.

Interpretation 1

The candidates for the method call v.into_foo() are the following, in order:

  • IntoFoo::into_foo(v) <- works
  • IntoFoo::into_foo(&v) <- doesn't work
  • IntoFoo::into_foo(&mut v) <- doesn't work
  • IntoFoo::into_foo(*v) <- doesn't work
  • IntoFoo::into_foo(&*v) <- doesn't work
  • IntoFoo::into_foo(&mut *v) <- works

It would seem that according to the documentation the first one should be called (IntoFoo::into_foo(v)), whereas in reality the last one is called (IntoFoo::into_foo(&mut *v)).

So this interpretation doesn't match reality.

Interpretation 2

Now I'm going for a more literal interpretation of what the page says.

First the list of types is formed, in order:

  • &mut C <- works
  • &&mut C
  • &mut &mut C
  • C
  • &C
  • &mut C <- works

&mut C appears twice in the list. Whatever, that type is selected. Now we find methods that match that type, and there is only one: IntoFoo::into_foo(&mut C).

But under this second interpretation the documentation doesn't specify how the method call is translated into a function call. Is it IntoFoo::into_foo(v) or IntoFoo::into_foo(&mut *v)? How is this decided?

2 Likes

I think it's probably calling

// Fresh inference variable
<&'_ mut C as IntoFoo>::into_foo(v)

Instead of

// Infers to be the exact type of v, including lifetime
<_ as IntoFoo>::into_foo(v);

...but that's just a guess and this is another example of Rust having no spec or nuanced documentation at the language level.

2 Likes