Why does += require manual dereference when AddAssign() does not?

    let mri = &mut 0;
    mri += 1;

gives the following error (playground):

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:

    mri.add_assign(1);
    AddAssign::add_assign(mri, 1);

According to the language reference section on compound assignment expressions, I'd expect mri += 1 and AddAssign::add_assign(mri, 1) to be equivalent.

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?

You can do this:

let mri = &mut 0;
*mri += 1;

...because you don't want to add to the the reference itself, but to the value it is pointing to.

Meanwhile, the add_assign() function takes a reference as parameter:

pub trait AddAssign<Rhs = Self> {
    fn add_assign(&mut self, rhs: Rhs);
}

...and it obviously needs to take a &mut, because otherwise it couldn't modify the value :slight_smile:


The documentation even says:

For example, the following expression statements in example are equivalent:

fn example() {
  a1 += a2;
  AddAssign::add_assign(&mut a1, a2);
}

...which means those should equivalent to:

fn example() {
  *a1 += a2;
  AddAssign::add_assign(a1, a2);
}

All that you said is true (and i understand it), but it's not the question I'm asking. Per the reference I linked and you quoted:

mri += 1 // should desugar to...
AddAssign::add_assign(&mut mri, 1);

But that's definitely not the case, because that fails…

error[E0277]: cannot add-assign `_` to `&mut {integer}`
 --> src/lib.rs:9:37
  |
9 |     AddAssign::add_assign(&mut mri, 1); // build failure
  |     ---------------------           ^ no implementation for `&mut {integer} += _`

If instead, it desugars to…

AddAssign::add_assign(&mut *mri, 1);

…which does work, then why is there an implicit dereference operation that doesn't occur in the += operator case?

Because method calls automatically reference or dereference as much as needed.

Again, add_assign() needs to take a &mut, so that it actually can modify the value.

So, this code:

let mri = 0;
mir += 1;

...implicitly translates to:

let mri = 0;
AddAssign::add_assign(&mut mri, 1);

With the exactly same logic, that code:

let mri = &mut 0;
mir += 1;

...would implicitly translates to:

let mri = &mut 0;
AddAssign::add_assign(&mut mri, 1);

You would be passing a reference to a reference to an i32, but add_assign() expects just a reference to an i32.

1 Like

So why does that not apply after the += is desugared into the add_assign call?

Yeah, and in my original example mri is a mutable reference to int

let mri = &mut 0;

So, add_assign should already have what it needs

Your responses are changing the code to

let mri = 0;

Which is not what I'm asking

What I'm saying is, if this code:

let mri = 0;
mri +=1;

...translates into:

let mri = 0;
add_assign(&mut mir); // <-- implicit reference added

(which it obviously must do, because without a &mut the add_assign() couldn't modify the value!)

...then, with the exactly same logic/rules, your code:

let mri = &mut 0;
mri +=1;

...translates into:

let mri = &mut 0;
add_assign(&mut mri);

Only that, in the second (your) code, we are effectively passing a reference to a reference to an i32.

But add_assign() still expects a reference to an i32 :slight_smile:


Meanwhile, my code:

let mri = &mut 0;
*mri +=1;

...effectively translates into:

let mri = &mut 0;
add_assign(mri);

...which is exactly what we need for add_assign(), because mri already is a reference!

1 Like

No, I think your translation is wrong (or at least it doesn't literally match the docs). If you take the example from the reference:

  a1 += a2;
  AddAssign::add_assign(&mut a1, a2);

and replace a2 with 1 and a1 with either mri or *mri, you get

  AddAssign::add_assign(&mut mri, 1);  // (1)
// or
  AddAssign::add_assign(&mut *mri, 1); // (2)

1 fails with:

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.

The following three statements are all equivalent and they all succeed:

    *mri += 1;
    AddAssign::add_assign(&mut *mri, 1);
    AddAssign::add_assign(mri, 1);

The following two statements are also equivalent and they both fail to compile because the types are wrong:

    mri += 1;
    AddAssign::add_assign(&mut mri, 1);
3 Likes

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,

isn't the full story.

1 Like

However! That still doesn't make

AddAssign::add_assign(&mut mri, 1); 

or

mri += 1; 

work.

mri = &mut 0;
AddAssign::add_assign(&mut mri, 1);

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?

1 Like

Yes, the . syntax has implicit auto-derefence rules, regular function calls and the += operator don't have any auto-derefence rules.

When the compiler sees this:

(&mut mri).add_assign(1); 

it tries to compile it in various ways, in particular:

AddAssign::add_assign(&mut mri, 1);

but this wouldn't work, so it also tries:

AddAssign::add_assign(*&mut mri, 1);

which does work.

3 Likes

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.

It's documented under Method Call Expressions.

The Deref coercion that The Book is talking about is something different that doesn't apply here.

1 Like

Thanks for the pointer to the Method-call expressions docs.

And where is that documented?

It's documented under Type coersions.

Interestingly, this works:

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.

2 Likes

I've been confused about that line for approximately :infinity:, but that's not what it means.

Deref coercion definitely still works on the receiver using fully qualified syntax. And for DerefMut too.

I think this is more a matter of trait impl resolution.


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).

3 Likes

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:

  1. T's inherent methods (methods implemented directly on T).
  2. 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.

Edit: I have filed a ticket about this.

Nevermind, here's an example that AddAssign::add_assign(&mut $LHS, $RHS) can't be a literal desugaring of $LHS += $RHS.

4 Likes