Nathaniel Waisbrot
Nathaniel Waisbrot

Reputation: 24473

Correct implementation of Debug for dyn trait that uses pointers

I want to be able to compare two structs, when the structs contain a reference to a trait inside them. I only care about knowing that the two references are to the same memory location. My code compiles, runs, and shows that the two references are not equal. I expected them to be equal, so I have a bug. But my Debug impl shows that they are equal, so I'm confused.

I haven't been able to make a self-contained repo of my bug, but I can reproduce my Debug problem:

#[derive(Debug, PartialEq)]
struct Intersection<'a> {
    intersected: &'a dyn Intersectable,
}

trait Intersectable {}
impl<'a> core::fmt::Debug for dyn Intersectable + 'a {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Intersectable{{{:?}}}", core::ptr::addr_of!(self))
    }
}
impl<'a> PartialEq for dyn Intersectable + 'a {
    fn eq(&self, other: &Self) -> bool {
        std::ptr::eq(self, other)
    }
}

#[derive(Debug, PartialEq)]
struct Sphere {}
impl Intersectable for Sphere {}

#[test]
fn comparison_bug() {
    let sphere = Sphere{};
    let other = Sphere{};
    let i = Intersection{ intersected: &sphere };

    assert_eq!(i.intersected, &sphere as &dyn Intersectable);
    assert_eq!(i.intersected, &other as &dyn Intersectable);
}

In the test, the assertion assert_eq!(i.intersected, &other as &dyn Intersectable); fails. This is correct -- I'm looking at two different instances of a Sphere struct, so the definitely are not equal.

The problem is that the message printed by assert_eq! says

assertion failed: `(left == right)`
  left: `Intersectable{0x7faebfa72188}`,
 right: `Intersectable{0x7faebfa72188}`

This is no good. I'm trying to print the address of each reference, so left and right should be different addresses (as the test correctly determined!). Why is my Debug printing the same address for both?

I also tried making my impl of PartialEq into addr_of!(self) == addr_of(other) and it behaves exactly the same (the test fails correctly but the Debug message appears to show that they are the same address).

Upvotes: 1

Views: 123

Answers (1)

kmdreko
kmdreko

Reputation: 59862

What you are currently doing is getting a pointer to the reference; you are getting addr_of!(self) but self is a reference. This would be a function local variable and therefore the address can be the same as another invocation. Instead, you want the address of what self refers to, use *self:

write!(f, "Intersectable{{{:?}}}", core::ptr::addr_of!(*self))

You can avoid using addr_of! though by using the pointer format specifier, "{:p}", instead:

write!(f, "Intersectable{{{self:p}}}")

However, your test is flawed because Sphere is a zero-sized type (ZST). This means it can take up no memory and therefore multiple ZSTs can occupy the same memory address. When I ran your test in --release mode, it gave sphere and other the same address and therefore your test succeeds when you're expecting it not to.

If Sphere is not actually intended to be a ZST (and just an artifact of minimization), then there's not much to worry about, but it is something to keep in mind when considering pointer equality.

As an additional note: you should avoid std::ptr::eq for comparing dyn Intersectable:

When comparing wide pointers, both the address and the metadata are tested for equality. However, note that comparing trait object pointers (*const dyn Trait) is unreliable: pointers to values of the same underlying type can compare inequal (because vtables are duplicated in multiple codegen units), and pointers to values of different underlying type can compare equal (since identical vtables can be deduplicated within a codegen unit).

Upvotes: 2

Related Questions