如何在“Arc”中比较trait对象?

7

想象一下我有以下内容:

let a : Arc<dyn SomeTrait> = getA();
let b : Arc<dyn SomeTrait> = getB();

现在,我想知道 ab 是否持有同一个对象,但是下面两种方法被Clippy标记为 比较trait对象指针比较非唯一vtable地址

let eq1 = std::ptr::eq(a.as_ref(), b.as_ref());
let eq2 = Arc::ptr_eq(&a, &b);

如何推荐检查特质对象的相等性?


对象可以有相同的地址,但在某种意义上仍然不是“相同”的对象。Arc规则排除了零大小类型和常见初始序列,但repr(transparent)仍然适用。我认为“相同”是什么意思很重要。 - trent
1
我希望Rust能够使它们的虚函数表变得独特:/ 这段代码似乎是显而易见的事情,但不幸的是它是错误的。 - kmdreko
我将其升级为问题,因为我认为这是一个需要讨论并可以改进的小问题。 - Magix
2个回答

9

我认为解释为什么比较可能不明智很重要,因为根据您的用例,您可能不关心这些问题。

导致诸如(fat)指针比较被认为是反模式的一个简单原因是,这样的比较可能会产生令人惊讶的结果;对于布尔测试,只有两种非直观情况:

  • 误报(两个预期的不同事物仍然相等);

  • 漏报(两个预期的相等的事物最终不相等)。

显然,所有这些都与 期望 相关。执行的测试是指针等式:

  • 大多数人会期望如果两个指针指向相同的数据,则它们应该相等... 但在涉及 fat 指针时并不一定如此。这肯定会导致漏报

  • 一些人,特别是那些不习惯零大小类型的人,可能还会想象两个不同的实例必须必然位于不同的内存中,并且“因此”具有不同的地址。但是(零大小的)类型即使位于相同的地址上,也不可能重叠,因为这种重叠是零大小的!这意味着您可以在相同的地址上拥有“不同的此类实例”(顺便说一下,这也是许多语言不支持零大小类型的原因:失去了独特地址属性会有自己的缺点)。不了解此情况的人可能会观察到误报

例子

两个fat指针可以具有相等的数据指针,但比较结果却不相等

  • 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.

现在,语言中仅有的另一种使用fat pointers的情况是(指向)dyn Trait/特质对象。这些fat pointers携带的元数据是一个指向包含主要特定方法(fn指针)的结构体的引用,该结构体对应于数据的已擦除的原始类型:虚拟方法表,即vtable。

每当将(因此变得苗条的)指向具体类型的指针强制转换为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`

原来编译器(更准确地说,是当前编译单元)这样做时,它会创建自己的虚表。

这意味着如果不同的编译单元执行这样的强制转换,则可能涉及多个虚表,因此对它们的引用可能彼此不相等!

我确实能够在以下游乐场中重现这一点,该游乐场利用了src/{lib,main}.rs分别编译的事实:

在我撰写本文时,该游乐场失败并显示如下错误信息:

thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `0x5567e54f4047`,
 right: `0x5567e54f4047`', src/main.rs:14:9

如您所见,数据指针是相同的,而assert_eq!错误消息只显示这些内容(fat指针的Debug实现不显示元数据)。


两个不同的对象可能存在于相同的地址上

非常简单的展示:

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.

结论?

现在你已经了解了“胖指针比较”的注意事项,如果您想要执行比较(明知这违反了Clippy lint的建议),您可以做出选择,例如,在您的代码中的单个位置将所有的Arc<dyn Trait>实例"肥化"(强制转换为dyn) (这可以避免来自不同虚表的误报),并且没有涉及到零大小的实例(这可以避免误报)。

例如:

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)
        }
    }
}

不过,正如您所看到的,即使那时我似乎仍然依赖于关于当前虚表实例化的一些知识; 因此这是相当不可靠的。这就是为什么您应该仅执行Slim指针比较的原因。

TL,DR

首先将每个指针变薄 (&*arc as *const _ as *const ()):然后才有意义去比较指针。


3
检查特质对象相等性的推荐方法是什么?除了“不要那样做”之外,不确定是否有其他方法。可能的替代方法包括:将操作添加到特质中,作为内在操作或通过向实例添加唯一标识符(例如UUID),使特质公开该操作;使用nightly并转换为TraitObject,检查data成员是否相同;将fat指针转换为thin指针并进行比较。

为什么不这样做呢?:( 这样做有真正的原因被反对吗? - Magix
1
因为它充满了边缘情况,例如正如@trentctl所指出的,如果它们是ZSTs,或者如果它们是嵌套的(一个结构体和结构体的第一个成员在内存中具有相同的地址,并且可能实现相同的trait,这并不意味着它们是相同的对象,而且kmdreko提示vtable身份也不是微不足道的)。 - Masklinn
1
说实话,我认为我们应该集中精力解决使检查特质对象相等变得困难的问题,而不是阻止使用这种合法且有用的模式。 - Magix

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接