Let's imagine I have the following context :
let a : Arc<dyn SomeTrait> = getA();
let b : Arc<dyn SomeTrait> = getB();
Now, I would like to know whether a
and b
hold the same object, but the following two approaches are flagged by Clippy as comparing trait object pointers compares a non-unique vtable address
:
let eq1 = std::ptr::eq(a.as_ref(), b.as_ref());
let eq2 = Arc::ptr_eq(&a, &b);
What is the recommended way of checking for trait object equality ?
I think it is important to explain why the comparison can be ill-advised, since, depending on your use case, you may not be concerned by the problems.
The main reason for something as simple as a (fat) pointer comparison to be considered an anti-pattern is that such a comparison may yield surprising results; for a boolean test, there are only two unintuitive cases:
false positives (two expected distinct things nevertheless compare equal);
false negatives (two expected equal things end up not comparing equal).
Obviously here all relates to the expectation. The test performed is pointer equality:
most would then expect that if two pointers point to the same data then they should compare equal… This is not necessarily the case when talking about fat pointers. This can thus definitely lead to false negatives.
some, especially those not used to zero-sized types, may also imagine that two distinct instances must necessarily live in disjoint memory and "thus" feature different addresses. But (instances of) zero-sized types cannot possibly overlap, even if they live at the same address, since such overlap would be zero-sized! That means that you can have "distinct such instances" living at the same address (this is also, by the way, the reason why many languages do not support zero-sized types: losing this property of unique addresses has its own share of caveats). People unaware of this situation may thus observe false positives.
There is a very basic example of it. Consider:
let arr = [1, 2, 3];
let all = &arr[..]; // len = 3, data_ptr = arr.as_ptr()
let first = &arr[.. 1]; // len = 1, data_ptr = arr.as_ptr()
assert!(::core::ptr::eq(all, first)); // Fails!
This is a basic example where we can see that the extra metadata bundled within a fat pointer (hence their being dubbed "fat") may vary "independently" of the data pointer, leading to these fat pointers then comparing unequal.
Now, the only other instance of fat pointers in the language are (pointers to) dyn Trait
s / trait objects. The metadata these fat pointers carry is a reference to a struct containing, mainly, the specific methods (as fn
pointers) of the trait corresponding to the now erased original type of the data: the virtual method table, a.k.a., the vtable.
Such a reference is automagically produced by the compiler every time a (thus slim) pointer to a concrete type is coerced to a fat pointer:
&42_i32 // (slim) pointer to an integer 42
as &dyn Display // compiler "fattens" the pointer by embedding a
// reference to the vtable of `impl Display for i32`
And it turns out that when the compiler, or more precisely, the current compilation unit, does this, it creates its own vtable.
This means that if different compilation units perform such a coercion, multiple vtables may be involved, and thus the references to them may not all be equal to each other!
I have indeed been able to reproduce this in the following playground which (ab)uses the fact src/{lib,main}.rs
are compiled separately.
At the time of my writing, that Playground fails with:
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `0x5567e54f4047`,
right: `0x5567e54f4047`', src/main.rs:14:9
As you can see, the data pointers are the same, and the assert_eq!
error message only shows those (the Debug
impl of fat pointers does not show the metadata).
Very simple showcase:
let box1 = Box::new(()); // zero-sized "allocation"
let box2 = Box::new(()); // ditto
let vec = vec![(), ()];
let at_box1: *const () = &*box1;
let at_box2: *const () = &*box2;
let at_vec0: *const () = &vec[0];
let at_vec1: *const () = &vec[1];
assert_eq!(at_vec0, at_vec1); // Guaranteed.
assert_eq!(at_box1, at_box2); // Very likely.
assert_eq!(at_vec0, at_box1); // Likely.
Now that you know the caveats of fat pointer comparison, you may make the choice to perform the comparison nonetheless (knowingly silencing the clippy lint), if, for instance, all your Arc<dyn Trait>
instances are "fattened" (coerced to dyn
) in a single place in your code (this avoids the false negatives from different vtables), and if no zero-sized instances are involved (this avoids the false positives).
Example:
mod lib {
use ::std::rc::Rc;
pub
trait MyTrait { /* … */ }
impl MyTrait for i32 { /* … */ }
impl MyTrait for String { /* … */ }
#[derive(Clone)]
pub
struct MyType {
private: Rc<dyn 'static + MyTrait>,
}
impl MyType {
// private! /* generics may be instanced / monomorphized across different crates! */
fn new<T : 'static + MyTrait> (instance: T)
-> Option<Self>
{
if ::core::mem::size_of::<T>() == 0 {
None
} else {
Some(Self { private: Rc::new(instance) /* as Rc<dyn …> */ })
}
}
pub fn new_i32(i: i32) -> Option<Self> { Self::new(i) }
pub fn new_string(s: String) -> Option<Self> { Self::new(s) }
pub
fn ptr_eq (self: &'_ MyType, other: &'_ MyType)
-> bool
{
// Potentially ok; vtables are all created in the same module,
// and we have guarded against zero-sized types at construction site.
::core::ptr::eq(&*self.private, &*other.private)
}
}
}
But, as you can see, even then I seem to be relying on some knowledge about the current implementation of vtable instantiation; this is thus quite unreliable. That's why you should perform slim pointer comparisons only.
Thin each pointer first (&*arc as *const _ as *const ()
): only then it is sensible to compare pointers.
What is the recommended way of checking for trait object equality ?
Not sure there's one, aside from "don't do that". Possible alternatives:
TraitObject
and checking if the data
members are identical.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With