6
use std::ops::Add;

#[derive(Debug)]
struct Sample {
    pub value: usize,
}
impl std::ops::Add<&Sample> for &Sample {
    type Output = Sample;

    fn add(self, other: &Sample) -> Sample {
        Sample {
            value: self.value + other.value,
        }
    }
}

fn main() {
    let mut a = Sample { value: 0 };
    let b = Sample { value: 1 };

    let mut_a = &mut a;
    let immut_b = &b;

    println!("Works: {:?}", mut_a.add(immut_b));
    println!("And this works: {:?}", immut_b.add(mut_a));
    println!("Also works:: {:?}", immut_b + mut_a);
    println!("Still works:: {:?}", &*mut_a + immut_b);
    println!("Fails: {:?}", mut_a + immut_b);
}

In this codeadd takes two &Samples. In the main method, I create two references to two different Samples, one mutable and one immutable. I can call explicitly call add on them in either order, but if I use the + operator, it complains if the mutable reference comes first. (Re-borrowing it as immutable also makes it work.) I would expect a mutable reference to be valid anywhere an immutable one is. What gives?

Deepening the mystery, I tried to do the same with basic i32s -

    let mut x: i32 = 0;
    let y: i32 = 1;
    let mut_x = &mut x;
    let immut_y = &y;
    println!("But this doesn't work? {:?}", immut_y + mut_x);
    println!("Or this? {:?}", mut_x + immut_y);
    println!("And this does? {:?}", mut_x.add(immut_y));
    println!("But not this? {:?}", immut_y.add(mut_x));S

And got a completely different unexpected behavior - now I can't add them in either order with the + operator, and the explicit call only works if the function is called on the mutable.

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
Edward Peters
  • 3,623
  • 2
  • 16
  • 39

1 Answers1

1

For the first snippet, the case is that while the method calls are resolved with auto-dereferencing, overloaded operators are not.


println!("Works: {:?}", mut_a.add(immut_b));

Via auto-dereferencing, we are able to pass &mut Sample argument where &Sample is expected. In order to resolve which method is to be called, rust roughly does:

  • Create candidate list for method call: [&mut Sample, &&mut Sample, &mut &mut Sample, Sample, &Sample, &mut Sample].
  • And for each candidate type in the list, it looks for a method named add that receives exactly &Sample as self type. At the point which &Sample is found, rust automatically derefs/refs mut_a in order to make call work. So desugared code looks like:
<&Sample>::add(&*mut_a, immut_b);

This alone doesn't explain much about auto-dereferencing, you should check link above.


println!("And this works: {:?}", immut_b.add(mut_a));

Auto-dereferencing resolves method at very first step without any auto-deref/ref.


println!("Also works:: {:?}", immut_b + mut_a);

Since this isn't a method call expression, there is no auto-dereferencing at all. Implementation type of the Add trait is inferred. Equivalent to:

::std::ops::Add::add(immut_b, mut_a);

println!("Still works:: {:?}", &*mut_a + immut_b);

Same as above except there is no coercion for right operand.


println!("Fails: {:?}", mut_a + immut_b);

Rust is unable to infer trait's implementation type because it only looks for implementations of Add for &mut Sample, which doesn't exist. Unlike method call expression, there is no auto-dereferencing in this case, and rust doesn't do anything further to resolve. Roughly desugared code (also fails for same reason):

<&mut Sample as ::std::ops::Add<&Sample>>::add(mut_a, immut_b);
Iwa
  • 512
  • 1
  • 5
  • 18
  • Thanks, but few follow-ups: FIrst, is there a typo in `<&mut Sample as ::std::ops::Add<&Simple>>::add(mut_a, immut_b);`? That syntax looks strange to me. Second, do you know if there's a reason auto-referencing of operators should not be supported, or is it just not something anyone has gotten to yet? Third, why was the behavior different for the `i32` calls? – Edward Peters Jan 06 '23 at 17:48
  • Yes, `Simple` is typo. Other than that, syntax matches with my understanding. For second, I don't know. For third, `i32`,`&i32` behaves differently because those has [multiple](https://doc.rust-lang.org/std/primitive.i32.html#impl-Add%3C%26i32%3E-for-%26i32) implementations of `Add`. I avoid answering this because I'm unsure how resolution works in presence of multiple candidates. – Iwa Jan 06 '23 at 19:38