rust

Force "shallow equality" of a type by comparing pointers in a type's `PartialEq` instance


Pointer types in Rust, such as Box<T>, Rc<T> and Arc<T>, implement their Eq and Hash instance in a deep way, i.e. they call to the respective instances of T. This is different from what a C/C++ programmer would expect from pointer types, which compare always shallowly in those languages.

In some cases one may want to still compare shallowly two pointer types. Since implementing Eq and Hash for Rc<MyType> is not possible because it would be an orphan instance, then I figured out I may do it by implement the shallow comparison and hashing in the type's instance, as follows:

pub struct MyType {
    pub x: (),
}

impl PartialEq for MyType {
    fn eq(&self, other: &Self) -> bool {
        std::ptr::eq(self, other)
    }
}

impl Eq for MyType {}

impl Hash for MyType {
    fn hash<H: Hasher>(&self, state: &mut H) {
        (self as *const MyType).hash(state);
    }
}

In this way, Box<MyType>, Rc<MyType>, and Arc<MyType> would compare shallowly (and MyType directly as well).

However, I'm quite new to Rust so I'm not sure this approach is sensible.

  1. does comparing pointers in that way do what I expect, i.e., returning true when and only when I compare exactly the same object?
  2. is there any advice against such a practice? Does this patently violates some code quality guideline? Is this "wrong" in any concrete way?

Solution

  • Rust generally eschews the idea of equality by identity. In particular, due to the interaction between how identity is determined in other languages (pointer address) and Rust's implicit moves, this can cause the identity of values to change as they are moved around, which is typically not desired.

    As you correctly point out, storing a value in a heap allocation "locks" the pointer-based identity of the value. Well, mostly – you can still move out of a Box<T>, most other smart pointers provide a into_inner of some sort.

    Because of this, there are two ways you can approach this problem:

    1. Put your type on the heap and pin it there. If you do not want to expose this detail to consumers, you could wrap it in a newtype (e.g. have a MyTypeImpl as the "real" implementation, and a struct MyType(Pin<Box<MyType>>) on which you can implement hashing and equality).
    2. The far simpler approach, simply add your own identity concept. This can be trivially implemented using a static AtomicUsize from which you can dispense unique object identifiers. This does increase the size of each value and potentially impacts visibility (the identifier should be private so it cannot be tampered with).
    use std::{
        hash::{Hash, Hasher},
        sync::atomic::AtomicUsize,
    };
    
    pub struct MyType {
        id: usize,
        pub x: (),
    }
    
    impl MyType {
        pub fn new(x: ()) -> Self {
            static ID: AtomicUsize = AtomicUsize::new(0);
    
            Self {
                id: ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
                x,
            }
        }
    }
    
    impl PartialEq for MyType {
        fn eq(&self, other: &Self) -> bool {
            self.id == other.id
        }
    }
    
    impl Eq for MyType {}
    
    impl Hash for MyType {
        fn hash<H: Hasher>(&self, state: &mut H) {
            self.id.hash(state);
        }
    }