Search code examples
rust

Why does the equality operator for owned trait object move the right operand?


I have a trait where I've implemented PartialEq (and Eq) for dyn Trait. When I use the equality operator on Box<dyn Trait>, Rc<dyn Trait>, or Arc<dyn Trait> it causes the right operand to be moved instead of borrowed. Why? Among other things, this prevents using assert_eq! directly on such values.

The docs for comparison operators say:

Unlike the arithmetic and logical operators above, these operators implicitly take shared borrows of their operands, evaluating them in place expression context:

a == b;
// is equivalent to
::std::cmp::PartialEq::eq(&a, &b);

This means that the operands don't have to be moved out of.

I can't figure out why the right operand ends up getting moved in this case. Shouldn't the right operand end up in the eq method as &Box<dyn Trait> (i.e. a borrow)?

Repro: (Playground)

use std::{
    fmt,
    rc::Rc,
    sync::Arc,
};

trait MyTrait : fmt::Debug {
    fn get_val(&self) -> u32;
}

#[derive(Clone, Debug)]
struct MyStruct {
    val: u32,
}

impl MyTrait for MyStruct {
    fn get_val(&self) -> u32 {
        self.val
    }
}

impl PartialEq for dyn MyTrait {
    fn eq(&self, other: &Self) -> bool {
        self.get_val() == other.get_val()
    }
}
impl Eq for dyn MyTrait {}

fn main() {
    // same error if you change `Box` to `Rc` or `Arc`
    let left:  Box<dyn MyTrait> = Box::new(MyStruct { val: 0 });
    let right: Box<dyn MyTrait> = Box::new(MyStruct { val: 42 });

//  assert_eq!(left, right); // error: cannot move out of `*right_val` [...]

    let _ = left == right; // moves `right`
    let _ = right.get_val(); // error: use of moved value
}

I've found some relatively easy workarounds:

  • Explicitly desugar: left.eq(&right) (though doesn't help for assert_eq!)
  • Compare trait objects (i.e. deref/unwrap the owner): *left == *right
  • Compare owner references: &left == &right
  • Compare trait object references: &*left == &*right
  • Wrap in Option: Some(left) == Some(right) (limited helpfulness since the values still get moved -- into the Options)

But (a) it's a little unsatisfying to need them, and (b) I'd still like to understand why this is happening.


Solution

  • This is a compiler bug: issue #31740.

    Until the bug is fixed, you'll have to work around it. Either of your options works, though I'd definitely pick one that doesn't move its arguments.