error[E0368]: binary assignment operation `+=` cannot be applied to type `&mut {integer}`
--> src/lib.rs:5:5
|
5 | mri += 1;
| ---^^^^^
| |
| cannot use `+=` on type `&mut {integer}`
|
help: `+=` can be used on `{integer}` if you dereference the left-hand side
|
5 | *mri += 1;
| +
Adding an explicit deref fixes it, but both these variations without an explicit deref also work:
Based on the std::ops::AddAssign docs, this is using impl AddAssign<&i32> for i32, which means fn add_assign(&mut i32, rhs: i32). If there's an automatic deref happening, I'm not sure why it only applies to the += operator syntax and not explicitly calling add_assign. What's going on?
10 | AddAssign::add_assign(&mut mri, 1); // ok
| --------------------- ^ no implementation for `&mut {integer} += _`
| |
| required by a bound introduced by this call
and 2 understandably succeeds.
I understand rust semantics around references and mutability. I'm not trying to figure out how to make this work. My question here is about what the translation between operator syntax and function call actually is and why the auto deref rules seem to be different for += versus explicitly calling add_assign.
That much, I follow! But here's where it gets strange (or perhaps, not clearly documented):
AddAssign::add_assign(&mut mri, 1);
fails because the first arg (or the receiver if using . syntax) is type &mut &mut {integer}. Presumably auto deref rules are different for . syntax, because:
(&mut mri).add_assign(1);
does succeed, but only if you change the mri binding itself to be mutable like so:
let mut mri = &mut 0;
However! That still doesn't make
AddAssign::add_assign(&mut mri, 1);
or
mri += 1;
work.
So…my question is: why are the auto auto deref rules different between these cases? Clearly,
add_assign() takes a &mut reference to the "thing" that it is supposed to increment.
For example, in order to increment a i32, we must pass a &mut i32.
...but, in the code snippet above, you are passing a &mut &mut i32 reference into add_assign(), which means that you are asking add_assign() to add 1 to an &mut i32 reference.
Adding an i32 to a &mut i32 simply is not supposed to work! What should the result of that be?
Thank you! This is what I'm asking. Where's that documented?
There's a bit of a conflict between the language reference, which says very little (unless it's somewhere else):
If the type of the container operand implements Deref or DerefMut depending on whether the operand is mutable, it is automatically dereferenced as many times as necessary to make the field access possible. This processes is also called autoderef for short.
and The Book (which seems to imply . syntax is not special):
It happens automatically when we pass a reference to a particular type’s value as an argument to a function or method that doesn’t match the parameter type in the function or method definition.
fn foo(a: &mut i32, b: i32) {
AddAssign::add_assign(a, b);
}
fn main() {
let mut mri = &mut 0;
foo(&mut mri, 1); // works due to Deref coercion
}
I think this doesn't work if you call AddAssign::add_assign directly because the Deref coercion doesn't apply to the self parameters in methods. That's somewhat confusingly documented on the same page:
For method calls, the receiver (self parameter) can only take advantage of unsized coercions.
It's a bit confusing because this exception is also true if you use the explicit AddAssign::add_assign(...) notation rather than the method call notation, which the docs don't clearly state.
More generally, I've learned to be skeptical when the documentation says operator X is the same as method call Y. Operator lookup is it's own underdocumented distinct thing from both method calls and method dispatch; as far as I'm aware, there's no desugaring that universally works for at least some operators.
That said, I haven't find a particular counter example for this case (AddAssign).
I am trying to figure out how this resolution works by reading the documentation.
Then, for each candidate type T, search for a visible method with a receiver of that type in the following places:
T's inherent methods (methods implemented directly on T).
Any of the methods provided by a visible trait implemented by T. If T is a type parameter, methods provided by trait bounds on T are looked up first. Then all remaining methods in scope are looked up.
I think this documentation is wrong because the self parameter type can be different from the type on which the trait or an inherent method is implemented. For instance, the type of self can be &T in an impl T which contradicts this algorithm.