如何比较“Arc”中的特征对象?
How to compare trait objects within an `Arc`?
假设我有以下上下文:
let a : Arc<dyn SomeTrait> = getA();
let b : Arc<dyn SomeTrait> = getB();
现在,我想知道 a
和 b
是否持有相同的对象,但是以下两种方法被 Clippy 标记为 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 ?
不确定有没有,除了“不要那样做”。可能的备选方案:
- 将操作添加到特征,作为内部操作或通过向特征公开的实例(例如 UUID)添加唯一标识符。
- 使用 nightly 和 transmuting to a
TraitObject
并检查 data
成员是否相同。
- casting the fat pointer to a thin pointer and comparing that
我认为解释 为什么 这种比较是不明智的,因为根据您的用例,您可能不会担心这些问题。
像(胖)指针比较这样简单的事情被认为是反模式的主要原因是这样的比较可能会产生令人惊讶的结果;对于布尔测试,只有两种不直观的情况:
误报(两个 预期 不同的事物仍然比较相等);
假阴性(两个预期相等的东西最终比较不相等)。
显然这里都涉及到期望。执行的测试是指针相等:
大多数人会期望如果两个指针指向相同的数据,那么它们应该比较相等…情况不一定如此在谈论胖指针时。因此,这肯定会导致 漏报 .
一些人,尤其是那些不习惯零大小类型的人,可能还会想象两个不同的实例必须存在于不相交的内存中,并且“因此”具有不同的地址。但是零大小类型(的实例)不可能重叠,即使它们位于同一地址,因为这种重叠将是零大小的!这意味着您可以在同一地址拥有“不同的此类实例”(顺便说一句,这也是许多语言不支持零大小类型的原因:丢失这个 属性 的唯一地址有其自己的警告份额)。因此,不知道这种情况的人可能会观察到 误报。
例子
两个 fat 指针可以具有相等的数据指针但比较不相等
有一个非常基本的例子。考虑:
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!
这是一个基本示例,我们可以在其中看到 捆绑在胖指针中的额外元数据(因此被称为“胖”)可能会“独立”地改变数据指针,导致这些胖指针然后比较不相等。
现在,该语言中唯一的其他胖指针实例是(指向)dyn Trait
s / trait 对象。 metadata 这些胖指针携带的是对结构的引用,主要包含与现在已删除的原始类型相对应的特征的特定方法(作为 fn
指针)数据:虚拟方法table、a.k.a.、vtable.
每次将指向具体类型的(因此是苗条的)指针强制转换为胖指针时,编译器都会自动生成这样的引用:
&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`
事实证明,当编译器或更准确地说,当前编译单元执行此操作时,它会创建自己的 vtable.
这意味着如果不同的编译单元执行这样的强制转换,可能会涉及多个vtable,并且因此对它们的引用可能并不都相等!
我确实能够在下面的游乐场中重现这一点,它(ab)使用了 src/{lib,main}.rs
单独编译的事实。
在我撰写本文时,Playground 失败并显示:
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `0x5567e54f4047`,
right: `0x5567e54f4047`', src/main.rs:14:9
如您所见,数据指针是相同的,assert_eq!
错误消息仅显示那些(胖指针的 Debug
impl 不显示元数据)。
两个不同的对象可能位于同一地址
非常简单的展示:
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
)(这避免了来自不同 vtables 的漏报),如果没有零大小涉及实例(这避免了误报)。
示例:
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)
}
}
}
但是,如您所见,即便如此,我似乎仍依赖一些关于 vtable 实例化的 current 实现的知识;因此,这是非常不可靠的。这就是为什么您应该只执行 slim 指针比较。
TL,DR
首先细化每个指针(&*arc as *const _ as *const ()
):只有这样比较指针才是明智的。
假设我有以下上下文:
let a : Arc<dyn SomeTrait> = getA();
let b : Arc<dyn SomeTrait> = getB();
现在,我想知道 a
和 b
是否持有相同的对象,但是以下两种方法被 Clippy 标记为 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 ?
不确定有没有,除了“不要那样做”。可能的备选方案:
- 将操作添加到特征,作为内部操作或通过向特征公开的实例(例如 UUID)添加唯一标识符。
- 使用 nightly 和 transmuting to a
TraitObject
并检查data
成员是否相同。 - casting the fat pointer to a thin pointer and comparing that
我认为解释 为什么 这种比较是不明智的,因为根据您的用例,您可能不会担心这些问题。
像(胖)指针比较这样简单的事情被认为是反模式的主要原因是这样的比较可能会产生令人惊讶的结果;对于布尔测试,只有两种不直观的情况:
误报(两个 预期 不同的事物仍然比较相等);
假阴性(两个预期相等的东西最终比较不相等)。
显然这里都涉及到期望。执行的测试是指针相等:
大多数人会期望如果两个指针指向相同的数据,那么它们应该比较相等…情况不一定如此在谈论胖指针时。因此,这肯定会导致 漏报 .
一些人,尤其是那些不习惯零大小类型的人,可能还会想象两个不同的实例必须存在于不相交的内存中,并且“因此”具有不同的地址。但是零大小类型(的实例)不可能重叠,即使它们位于同一地址,因为这种重叠将是零大小的!这意味着您可以在同一地址拥有“不同的此类实例”(顺便说一句,这也是许多语言不支持零大小类型的原因:丢失这个 属性 的唯一地址有其自己的警告份额)。因此,不知道这种情况的人可能会观察到 误报。
例子
两个 fat 指针可以具有相等的数据指针但比较不相等
有一个非常基本的例子。考虑:
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!
这是一个基本示例,我们可以在其中看到 捆绑在胖指针中的额外元数据(因此被称为“胖”)可能会“独立”地改变数据指针,导致这些胖指针然后比较不相等。
现在,该语言中唯一的其他胖指针实例是(指向)dyn Trait
s / trait 对象。 metadata 这些胖指针携带的是对结构的引用,主要包含与现在已删除的原始类型相对应的特征的特定方法(作为 fn
指针)数据:虚拟方法table、a.k.a.、vtable.
每次将指向具体类型的(因此是苗条的)指针强制转换为胖指针时,编译器都会自动生成这样的引用:
&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`
事实证明,当编译器或更准确地说,当前编译单元执行此操作时,它会创建自己的 vtable.
这意味着如果不同的编译单元执行这样的强制转换,可能会涉及多个vtable,并且因此对它们的引用可能并不都相等!
我确实能够在下面的游乐场中重现这一点,它(ab)使用了 src/{lib,main}.rs
单独编译的事实。
在我撰写本文时,Playground 失败并显示:
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `0x5567e54f4047`,
right: `0x5567e54f4047`', src/main.rs:14:9
如您所见,数据指针是相同的,assert_eq!
错误消息仅显示那些(胖指针的 Debug
impl 不显示元数据)。
两个不同的对象可能位于同一地址
非常简单的展示:
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
)(这避免了来自不同 vtables 的漏报),如果没有零大小涉及实例(这避免了误报)。
示例:
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)
}
}
}
但是,如您所见,即便如此,我似乎仍依赖一些关于 vtable 实例化的 current 实现的知识;因此,这是非常不可靠的。这就是为什么您应该只执行 slim 指针比较。
TL,DR
首先细化每个指针(&*arc as *const _ as *const ()
):只有这样比较指针才是明智的。