Search code examples
referencerustcoercion

Why does reference weakening from &mut occur in some trait method calls?


One of the few implicit conversions available in Rust is pointer weakening, which can turn a &mut T into a &T:

fn just_foo<T>(_: &T) {}

just_foo(&mut vec![1, 2, 3]);

However, this doesn't happen when matching traits. For instance, although the + operator with references as right-hand sided values is implemented for numeric types, they won't accept mutable references to the same type:

5 + &mut 5;
(&5) + &mut 5;

The error message:

error[E0277]: the trait bound `{integer}: std::ops::Add<&mut {integer}>` is not satisfied
--> src/main.rs:38:7
   |
38 | 5 + &mut 5;
   | ^ no implementation for `{integer} + &mut {integer}`
   |
   = help: the trait `std::ops::Add<&mut {integer}>` is not implemented for `{integer}`

error[E0277]: the trait bound `&{integer}: std::ops::Add<&mut {integer}>` is not satisfied
--> src/main.rs:43:10
   |
43 | (&5) + &mut 5;
   | ^ no implementation for `&{integer} + &mut {integer}`
   |
   = help: the trait `std::ops::Add<&mut {integer}>` is not implemented for `&{integer}`

For another, more intriguing example, I added an assortment of implementations of Add for a unit type Foo:

use std::ops::Add;

#[derive(Debug, Default)]
struct Foo;

impl Add<Foo> for Foo {
    type Output = Foo;
    fn add(self, _: Foo) -> Foo {
        Foo
    }
}

impl<'a> Add<&'a Foo> for Foo {
    type Output = Foo;
    fn add(self, _: &'a Foo) -> Foo {
        Foo
    }
}

impl<'a, 'b> Add<&'a Foo> for &'b Foo {
    type Output = Foo;
    fn add(self, _: &'a Foo) -> Foo {
        Foo
    }
}

Only to find that I can perform &Foo + &mut Foo, but not Foo + &mut Foo:

&Foo + &mut Foo; // ok
Foo + &mut Foo; // not ok

Full Playground

The second case is in line with the previous example above, but the first one isn't. It seems that the RHS &mut Foo was coerced to &Foo to match the implementation of &Foo + &Foo. It doesn't look either that other coercions are taking place, because the receiving type for &Foo as Add<&Foo> is already &Foo. I could also throw the syntactic sugar away and obtain the same outcome:

(&Foo).add(&mut Foo); // ok
Foo.add(&mut Foo); // not ok

Given that coercions, according to the Nomicon, are not supposed to happen when doing trait matching, why does this &Foo + &mut Foo work when &i32 + &mut i32 doesn't? Is it because there is a single implementation of Add for &Foo? If so, why does it make the compiler behave differently?


Solution

  • Is it because there is a single implementation of Add for &Foo?

    Let's see what happens when we add this implementation:

    impl<'b> Add<Foo> for &'b Foo {
        type Output = Foo;
        fn add(self, _: Foo) -> Foo {
            Foo
        }
    }
    

    Now &Foo + &mut Foo and &Foo + &mut &mut Foo fail to compile:

    error[E0277]: the trait bound `&Foo: std::ops::Add<&mut Foo>` is not satisfied
      --> src/main.rs:39:10
       |
    39 |     &Foo + &mut Foo;
       |          ^ no implementation for `&Foo + &mut Foo`
       |
       = help: the trait `std::ops::Add<&mut Foo>` is not implemented for `&Foo`
    
    error[E0277]: the trait bound `&Foo: std::ops::Add<&mut &mut Foo>` is not satisfied
      --> src/main.rs:40:10
       |
    40 |     &Foo + &mut &mut Foo;
       |          ^ no implementation for `&Foo + &mut &mut Foo`
       |
       = help: the trait `std::ops::Add<&mut &mut Foo>` is not implemented for `&Foo`
    

    So the answer is yes.

    If so, why does it make the compiler behave differently?

    When there is a single applicable implementation of Add<T> (or any other generic trait), the compiler doesn't need to infer T from the arguments; it has already resolved T based on that single implementation. Basically, it's as if the trait wasn't generic at all. Therefore, the coercions that work on non-generic arguments can be applied too.