Restrict auto-ref for method taking self?

Let's say for example:

trait MyTrait {
    fn consume(self);
}

struct S;

impl MyTrait for &S {
    fn consume(self) {
        println!("borrowed")
    }
}

fn main() {
    let s = S;

    s.consume();
    s.consume();
}

compiler will accept the code, which is, programmatically correct, but contradicts the intuition s.consume() would take ownership of s. One has to know the auto-ref rule (not in official docs) as well as the existence of ``impl MyTrait for &S``` to reason about why it compiles.

1 Like

How would you propose restricting auto-ref, without breaking existing code that uses this?

It might be something that starts as a clippy lint, but I fear you'll get a lot of false positives.

4 Likes

The fact that references are copyable comes up in other contexts too:

fn refcopy(copy: impl Copy) {
    
}

fn main() {
    refcopy(&String::new());
}
1 Like

Also note that adding

impl MyTrait for S {
    fn consume(self) {
        println!("owned")
    }
}

Will break your code and force you to borrow s instead of silently borrowing it.

1 Like

How would you propose restricting auto-ref, without breaking existing code that uses this?

For any single crate, if the author later introduce impl MyTrait for S, it will break existing code anyway. This situation could be improved if the compiler reject the code in the first place. How about adding a flag like how NLL was introduced?

fn main() {
    let s = S;
    let b = &s;

    b.consume();
    b.consume();
}

Which impl the compiler would pick?

well, b: &S so it will pick &S's impl. This is the forced borrow that I noted.

1 Like

Hmm..., then rules for inferring impl differ.

Here are the rules, given that var: T

  1. Is there an impl for T, if so use that impl, else go to step 2
  2. Is there an impl for &T, if so use that impl, else go to step 3,
  3. Is there an impl for &mut T, if so use that impl, else fail to compile

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=93eae555568c28622e3c60c981ace295

Very simple steps, and if you go down step 1, and fail to compile later the compiler will not backtrack and try another route. That would be absolutely terrible for compile times.

2 Likes

How about this?

trait MyTrait {
    fn consume(self);
}

struct S;

impl MyTrait for S {
    fn consume(self) {
        println!("owned")
    }
}

// impl MyTrait for &S {
//     fn consume(self) {
//         println!("borrowed")
//     }
// }

fn main() {
    let s = S;
    let b = &s;

    b.consume();
    b.consume();
}
error[E0507]: cannot move out of `*b` which is behind a shared reference
  --> src/main.rs:23:5
   |
23 |     b.consume();
   |     ^ move occurs because `*b` has type `S`, which does not implement the `Copy` trait

error[E0507]: cannot move out of `*b` which is behind a shared reference
  --> src/main.rs:24:5
   |
24 |     b.consume();
   |     ^ move occurs because `*b` has type `S`, which does not implement the `Copy` trait

error: aborting due to 2 previous errors

Step1: There is an impl for T, so the compiler automatically changes b to *b ?

Then uncomment impl MyTrait for &S, compiler still sees: step1, there is an impl for T, where T is &S, in this case?

Oh, I forgot about dereferencing, in this case T == &S, so we don't have any of T, &mut T, or &T so we try dereferencing because we have an impl for S. In this case because S: !Copy this fails.

So updated list,

  1. Is there an impl for T , if so use that impl, else go to step 2
  2. Is there an impl for &T , if so use that impl, else go to step 3,
  3. Is there an impl for &mut T , if so use that impl, else go to step 4
  4. Is T: Deref, then try dereferencing (this may fail if <T as Deref>::Target is not Copy or of T is not Copy)

disclaimer there may be other corner cases that I missed, but this should cover the vast majority of cases.

Where to put unsized coercions? e.g. [T; n] to [T]

Same as deref