Unexpected behavior of trait bounds

See the Rustlings issue where this is asked: Question related to trait bounds in excercise "as_ref_mut.rs" · Issue #1823 · rust-lang/rustlings · GitHub

TL;DR: Why does the compiler accept this with integers

fn num_sq<T: AsMut<u32>>(arg: &mut T) {
    *arg.as_mut() *= *arg.as_mut();
}

but not this with trait bounds?

use std::ops::MulAssign;

fn num_sq<T: AsMut<impl MulAssign + Copy>>(arg: &mut T) {
    *arg.as_mut() *= *arg.as_mut();
}

There is, surprisingly, a difference in evaluation order here.

  • When you use the *= operator on a primitive number, the right side is evaluated to obtain a value (of type u32 here), then the left side is evaluated to obtain the mutable place. The right side's temporary AsMut borrow has been dropped by the time the left side is evaluated.
  • When you use *= on a MulAssign, the left side is evaluated first, and its temporaries must therefore stay alive until the assignment is complete, which creates a borrow conflict when the right side is evaluated.

The Reference documents this:

If both types are primitives, then the modifying operand will be evaluated first followed by the assigned operand. It will then set the value of the assigned operand's place to the value of performing the operation of the operator with the values of the assigned operand and modifying operand.

Note: This is different than other expressions in that the right operand is evaluated before the left one.

Otherwise, this expression is syntactic sugar for calling the function of the overloading compound assignment trait of the operator (see the table earlier in this chapter). A mutable borrow of the assigned operand is automatically taken.

You can observe this evaluation order behavior by running a program that uses the same types but has separate inputs to avoid the conflict:

use std::convert::AsMut;
use std::ops;

struct NoisyDeref(u32);
impl AsMut<u32> for NoisyDeref {
    fn as_mut(&mut self) -> &mut u32 {
        eprintln!("as_mut {}", self.0);
        &mut self.0
    }
}

fn mul_u32<T: AsMut<u32>>(left: &mut T, right : &mut T) {
    *left.as_mut() *= *right.as_mut();
}
fn mul_trait<T: AsMut<impl ops::MulAssign + Copy>>(left: &mut T, right: &mut T) {
    *left.as_mut() *= *right.as_mut();
}

fn main() {
    eprintln!("With u32:");
    mul_u32(&mut NoisyDeref(1), &mut NoisyDeref(2));
    eprintln!("With MulAssign:");
    mul_trait(&mut NoisyDeref(3), &mut NoisyDeref(4));
}

This prints

With u32:
as_mut 2
as_mut 1
With MulAssign:
as_mut 3
as_mut 4

which shows the mixed ordering, neither left to right (1 2 3 4) nor right to left (2 1 4 3).

4 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.