Weird "cannot move" error

The following code (Rust Playground):

use std::sync::Arc;

trait Foo {}
impl PartialEq for dyn Foo {
    fn eq(&self, other: &Self) -> bool {
        true
    }
}

struct Bar(Arc<dyn Foo>);

impl PartialEq for Bar {
    fn eq(&self, other: &Self) -> bool {
        self.0 == other.0
    }
}

Generates the following error:

error[E0507]: cannot move out of `other` which is behind a shared reference
  --> src/lib.rs:14:19
   |
14 |         self.0 == other.0
   |                   ^^^^^^^ move occurs because `other.0` has type `Arc<dyn Foo>`, which does not implement the `Copy` trait
   |
help: clone the value to increment its reference count
   |
14 |         self.0 == other.0.clone()
   |                          ++++++++

Why? Doesn't == autoref?

The tuple dot operator .0 is whats moving out of other. You need to borrow the arc, like this:

use std::sync::Arc;

trait Foo {}
impl PartialEq for dyn Foo {
    fn eq(&self, other: &Self) -> bool {
        true
    }
}

struct Bar(Arc<dyn Foo>);

impl PartialEq for Bar {
    fn eq(&self, other: &Self) -> bool {
        &self.0 == &other.0
    }
}

Hmm… this is sorta weird though because usually, the == doesn’t try to move its arguments anyway :thinking:

Like: you can do

fn f() {
    let x: String = "hello".into();
    let y: String = "hello".into();
    println!("{:?}", x == y);
    println!("{:?}", x == y); // let's do it again, `x` and `y` are still there!
}

Or this, also works:

use std::sync::Arc;

struct Bar<T>(Arc<[T]>);

impl<T: PartialEq> PartialEq for Bar<T> {
    fn eq(&self, other: &Self) -> bool {
        self.0 == other.0
    }
}

This, too:

use std::sync::Arc;

struct Bar<T: ?Sized>(Arc<T>);

impl<T: PartialEq + ?Sized> PartialEq for Bar<T> {
    fn eq(&self, other: &Self) -> bool {
        self.0 == other.0
    }
}

same with Box

use std::sync::Arc;

trait Foo {}
impl PartialEq for dyn Foo {
    fn eq(&self, other: &Self) -> bool {
        true
    }
}

fn g<T: ?Sized + PartialEq>(x: Arc<T>, y: Arc<T>) {
    x == y;
    x == y; // okay
}
fn h(x: Arc<dyn Foo>, y: Arc<dyn Foo>) {
    x == y;
    x == y; // use of moved value
}

fn g2<T: ?Sized + PartialEq>(x: Box<T>, y: Box<T>) {
    x == y;
    x == y; // okay
}
fn h2(x: Box<dyn Foo>, y: Box<dyn Foo>) {
    x == y;
    x == y; // use of moved value
}

Found an existing issue at least:


Apparently, the exact conditions of when this issue appears are somehow related to unsizing coercions.

5 Likes

To solve the original problem:

impl PartialEq for Bar {
    fn eq(&self, other: &Self) -> bool {
        *self.0 == *other.0
    }
}

This dereferences the Arc<dyn Foo> to obtain dyn Foos, so the wanted impl PartialEq for dyn Foo applies exactly (no implicit dereferencing, and only the implicit reference baked into ==).

1 Like

With Box<dyn Trait> you can work around it by implementing PartialEq<&Self> for Box<dyn Trait>, but that's only possible because Box is fundamental.

I found that workaround here; that issue is the earliest one about the bug that I know about. Various dupes are mentioned in the issue.

It seems like == can coerce the right argument. Which is mostly relevant for unsizing coercions (but not exclusively). And unfortunately it coerces by value. [E.g. Deref-coercions don't really come into play, because &mut _ and &_ already have so many different PartialEq implementations, so that (at least typically), coercions are removed by the compiler before they could ever materialize, in order to avoid ambiguous types.]

I think any fix to the situation would at a minimum have to break code like this:

use std::cell::Cell;

trait Trait {}

impl PartialEq for dyn Trait + '_ {
    fn eq(&self, _: &dyn Trait) -> bool {
        true
    }
}

fn f<'a, 'b: 'a>(x: Cell<&'a (dyn Trait + 'a)>, y: Cell<&'b (dyn Trait + 'b)>) {
    x == y;
}

because even if we added a case to "avoid the coercion if the type isn't changed", such a check would realistically not be able to consider lifetimes.

Additionally, it would of course affect some drop orders, though realistically, those were super surprising anyways. (It drops the right argument early, but not the left).

Here's a case with slices which doesn't work with a PartialEq::eq(…)-desugaring either

fn f(x: Box<[i32]>, y: Box<[i32; 3]>) {
    x == y; // works (but consumes `y` by value)
}

(nice that it works, but also kind-of confusing, especially regarding the drop-order)

Here's a case with reference to pointer coercion:

fn f(x: *const u8, y: &mut u8) {
    x == y;
    // PartialEq::eq(&x, &y); doesn't work
}

though it's less bad because this still doesn't consume y (it produces &raw const *y in the MIR).


IMHO, long-term it could be reasonable to fully replace == with the PartialEq::eq(&left, &right) desugaring that the reference promises, at least over an edition, assuming the broken cases can probably all be fixed by rewriting left == right into left == (right as _), which is easily machine-applicable.

2 Likes