Relation between AddAssign and '+=' Operator

Viewed 262

My understanding so far was that in Rust, operators are basically syntactic sugar for trait method calls. In particular, I thought that a += b was equivalent to writing a.add_assign(b). I was very suprised today to hear the following from rustc (1.44.1):

error[E0368]: binary assignment operation `+=` cannot be applied to type `&mut u8`
 --> src/main.rs:2:5
  |
2 |     a += b;
  |     -^^^^^
  |     |
  |     cannot use `+=` on type `&mut u8`
  |
help: `+=` can be used on 'u8', you can dereference `a`
  |
2 |     *a += b;
  |     ^^

The code responsible for the error message is (Playground)

fn test_add_assign(a: &mut u8, b: u8) {
    a += b;
}

fn main() {
    let mut test = 1;
    test_add_assign(&mut test, 1);
    assert_eq!(test, 2);
}

Now, the compiler is correct, writing *a += b works and also correctly assigns the new variable to a. To my suprise, however, also a.add_assign(b) works perfectly fine without the need to dereference a (Playground):

fn test_add_assign(a: &mut u8, b: u8) {
    a.add_assign(b);
}

Given that the documentation for AddAssign simply states

The addition assignment operator +=.

I am wondering: What is the relationship between AddAssign and the += operator, if it is not basically syntactic sugar for calling the trait method?

2 Answers

I thought that a += b was equivalent to writing a.add_assign(b).

Not quite, a += b is actually translated to ::std::ops::AddAssign::add_assign(&mut a, b). In your example that means you would pass an &mut &mut u8 as the first parameter.

If you think about it, this makes sense. A standard assignment to an integer variable i is written as i = 3;. If you want to make this a function call instead, you need to pass a mutable reference to i to the function so it can actually modify the value of i. The same applies to augmented assignments.

Note that the method call syntax a.add_assign(b) happens to work in this case, because method calls treat the receiver in a special way. The compiler looks for a matching method by implicitly borrowing and dereferencing the receiver until a match is found. Method calls for traits with a type parameter are special again, since the search may even continue to find a match for the other parameters to that method as well (which I don't think is documented in the Rust reference at this time).

You are right, more or less.

The issue that I think it is confusing you is the automatic deref in method functions calls. It is detailed in this other question, but basically it says that you can call a member function either with a value or a reference, or a reference to a reference, and it will just work:

let x = 42;
let _ = x.to_string(); //ok
let _ = (&x).to_string(); //ok
let r = &x;
let _ = r.to_string(); //ok
let _ = (*r).to_string(); //ok

But when using operators the automatic deref does not apply. So:

let mut x = 42;
x += 1; //ok;
x.add_assign(1); //ok
let r: &mut i32 = &mut x;
*r += 1; //ok
r += 1; //error: &mut i32 does not implement AddAssign
r.add_assign(1); //ok: r is auto-dereffed

Note how the left expression of += must be the value to be modified (an rvalue), not a reference to that value. Then, actually when you write a += b it is equivalent to AddAssign::add_assign(&mut a, b)

Related