4

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?

msrd0
  • 7,816
  • 9
  • 47
  • 82

2 Answers2

3

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

Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
  • Thanks for the explanation, this actually makes sense (and should be documented somewhere). Is there any reason for not applying the receiver treatment of methods for (assignment) operators, or is this just an oversight in the language? – msrd0 Jul 11 '20 at 19:11
  • @msrd0 That's definitely not an oversight. Rust is in general very cautious with implicit type conversions, since they can seem like magic. I'd actually think we'd be better off with even _less_ of this convenience magic, since the magic always comes at a cost. In the case of the receiver, the language designers deemed it just _too_ inconvenient if you had to write things like `(&mut x).method()` to pass the receiver by mutable reference. I personally wouldn't have minded the more explicit form. – Sven Marnach Jul 11 '20 at 20:45
2

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)

rodrigo
  • 94,151
  • 12
  • 143
  • 190