如果我已经实现了Drop,为什么使用PhantomData来通知编译器结构拥有泛型?

Cod*_*ich 6 rust

Rustonomicon的指南中PhantomData,有一部分关于如果a Vec-like struct有*const T字段会发生什么,但是没有PhantomData<T>:

下拉检查器将慷慨地确定Vec<T>不具有任何类型的值T.这将使得它得出结论,它不需要担心在析构函数中Vec丢弃任何T用于确定跌落检查稳健性的任何东西.这反过来又允许人们使用Vec析构函数创建不健全的东西.

这是什么意思?如果我实现Drop一个结构并手动销毁T它中的所有s,为什么我应该关心编译器是否知道我的struct拥有一些Ts?

pnk*_*lix 15

PhantomData<T>Vec<T>(通过间接持有Unique<T>RawVec<T>)进行通信,以使得所述载体可以拥有的情况下,编译器T,因此,载体可运行的析构函数T当载体被丢弃.


深入探讨:我们在这里有多种因素:

  • 我们有一个Vec<T>它有一个impl Drop(即析构函数实现).

  • 根据RFC 1238的规则,这通常意味着在实例中Vec<T>发生的实例和任何生命周期之间的关系T,要求所有生命周期内都T严格超过向量.

  • 但是,析构函数通过使用特殊的不稳定属性(参见RFC 1238RFC 1327)专门为这个析构函数(自身)Vec<T>选择了这个语义.这允许向量保存具有与向量本身相同的生命周期的引用.这被认为是合理的; 毕竟,只要一个重要的警告成立,向量本身就不会取消引用这些引用所指向的数据(它所做的只是删除值并释放后备数组).Vec<T>

  • 重要的警告:虽然向量本身在破坏自身时不会在其包含的值内取消引用指针,但它丢弃向量所持有的值.如果这些类型的值T本身具有析构函数,那么这些析构函数T就会被运行.如果那些析构函数访问其引用中保存的数据,那么如果我们在这些引用中允许悬空指针,那么我们就会遇到问题.

  • 因此,深入探讨:我们确认给定结构的dropck有效性的方式S,我们首先仔细检查它S自己是否有一个impl Drop for S(如果是这样,我们S就其类型参数强制执行规则).但是,即使这一步之后,我们再递归下降到结构S本身,它的每个字段的仔细检查犹太根据dropck这一切都是.(注意,即使使用S标记的类型参数,我们也会这样做#[may_dangle].)

  • 在这种特定情况下,我们有一个Vec<T>(间接通过RawVec<T>/ Unique<T>)拥有一个类型值的集合,T以原始指针表示*const T.但是,编译器不会附加所有权语义*const T; 单独在结构中的那个字段S意味着和之间没有关系,因此在类型内的生命期关系和(至少从dropck的观点)方面强制执行约束.STST

  • 因此,如果Vec<T>*const T,递归下降到载体的结构将无法捕捉的矢量和的实例之间的所有制关系T的载体中包含.结合#[may_dangle]属性on T,将导致编译器接受不健全的代码(即T最终尝试访问已经解除分配的数据的析构函数).

  • 但是:Vec<T>完全包含*const T.还有一个PhantomData<T>,而且传达给编译器"哎,你尽管可以假定(由于#[may_dangle] T)的析构函数Vec将无法访问的数据T时,矢量被删除,它仍然可能是一些析构函数T 本身T作为向量的访问数据被删除."

结束效果:给定Vec<T>,如果T 没有析构函数,那么编译器为您提供了更大的灵活性(即,它允许向量保存数据,引用与向量本身相同的时间量的数据,甚至虽然这些数据可能会在向量之前被拆除).但是如果T 确实有析构函数(并且析构函数没有与编译器进行通信而不会访问任何引用的数据),则编译器更严格,要求任何引用的数据严格超过向量(从而确保T运行的析构函数,所有引用的数据仍然有效).


如果一个人想尝试通过具体的勘探明白这一点,你可以尝试比较编译器在其治疗,在他们使用的变化很小容器类型的不同之处#[may_dangle]PhantomData.

这里有一些示例代码,我已经用它来说明这一点:

// Illustration of a case where PhantomData is providing necessary ownership
// info to rustc.
//
// MyBox2<T> uses just a `*const T` to hold the `T` it owns.
// MyBox3<T> has both a `*const T` AND a PhantomData<T>; the latter communicates
// its ownership relationship with `T`.
//
// Skim down to `fn f2()` to see the relevant case, 
// and compare it to `fn f3()`. When you run the program,
// the output will include:
//
// drop PrintOnDrop(mb2b, PrintOnDrop("v2b", 13, INVALID), Valid)
//
// (However, in the absence of #[may_dangle], the compiler will constrain
// things in a manner that may indeed imply that PhantomData is unnecessary;
// pnkfelix is not 100% sure of this claim yet, though.)

#![feature(alloc, dropck_eyepatch, generic_param_attrs, heap_api)]

extern crate alloc;

use alloc::heap;
use std::fmt;
use std::marker::PhantomData;
use std::mem;
use std::ptr;

#[derive(Copy, Clone, Debug)]
enum State { INVALID, Valid }

#[derive(Debug)]
struct PrintOnDrop<T: fmt::Debug>(&'static str, T, State);

impl<T: fmt::Debug> PrintOnDrop<T> {
    fn new(name: &'static str, t: T) -> Self {
        PrintOnDrop(name, t, State::Valid)
    }
}

impl<T: fmt::Debug> Drop for PrintOnDrop<T> {
    fn drop(&mut self) {
        println!("drop PrintOnDrop({}, {:?}, {:?})",
                 self.0,
                 self.1,
                 self.2);
        self.2 = State::INVALID;
    }
}

struct MyBox1<T> {
    v: Box<T>,
}

impl<T> MyBox1<T> {
    fn new(t: T) -> Self {
        MyBox1 { v: Box::new(t) }
    }
}

struct MyBox2<T> {
    v: *const T,
}

impl<T> MyBox2<T> {
    fn new(t: T) -> Self {
        unsafe {
            let p = heap::allocate(mem::size_of::<T>(), mem::align_of::<T>());
            let p = p as *mut T;
            ptr::write(p, t);
            MyBox2 { v: p }
        }
    }
}

unsafe impl<#[may_dangle] T> Drop for MyBox2<T> {
    fn drop(&mut self) {
        unsafe {
            // We want this to be *legal*. This destructor is not 
            // allowed to call methods on `T` (since it may be in
            // an invalid state), but it should be allowed to drop
            // instances of `T` as it deconstructs itself.
            //
            // (Note however that the compiler has no knowledge
            //  that `MyBox2<T>` owns an instance of `T`.)
            ptr::read(self.v);
            heap::deallocate(self.v as *mut u8,
                             mem::size_of::<T>(),
                             mem::align_of::<T>());
        }
    }
}

struct MyBox3<T> {
    v: *const T,
    _pd: PhantomData<T>,
}

impl<T> MyBox3<T> {
    fn new(t: T) -> Self {
        unsafe {
            let p = heap::allocate(mem::size_of::<T>(), mem::align_of::<T>());
            let p = p as *mut T;
            ptr::write(p, t);
            MyBox3 { v: p, _pd: Default::default() }
        }
    }
}

unsafe impl<#[may_dangle] T> Drop for MyBox3<T> {
    fn drop(&mut self) {
        unsafe {
            ptr::read(self.v);
            heap::deallocate(self.v as *mut u8,
                             mem::size_of::<T>(),
                             mem::align_of::<T>());
        }
    }
}

fn f1() {
    // `let (v, _mb1);` and `let (_mb1, v)` won't compile due to dropck
    let v1; let _mb1;
    v1 = PrintOnDrop::new("v1", 13);
    _mb1 = MyBox1::new(PrintOnDrop::new("mb1", &v1));
}

fn f2() {
    {
        let (v2a, _mb2a); // Sound, but not distinguished from below by rustc!
        v2a = PrintOnDrop::new("v2a", 13);
        _mb2a = MyBox2::new(PrintOnDrop::new("mb2a", &v2a));
    }

    {
        let (_mb2b, v2b); // Unsound!
        v2b = PrintOnDrop::new("v2b", 13);
        _mb2b = MyBox2::new(PrintOnDrop::new("mb2b", &v2b));
        // namely, v2b dropped before _mb2b, but latter contains
        // value that attempts to access v2b when being dropped.
    }
}

fn f3() {
    let v3; let _mb3; // `let (v, mb3);` won't compile due to dropck
    v3 = PrintOnDrop::new("v3", 13);
    _mb3 = MyBox3::new(PrintOnDrop::new("mb3", &v3));
}

fn main() {
    f1(); f2(); f3();
}
Run Code Online (Sandbox Code Playgroud)

  • 感谢您如此详尽的回复,感觉就像丢失了 Rustonomicon 一章。如果我猜对了,一句话的答案可能是“让编译器检查 T 是否没有 `drop` 并让它具有与 `Vec&lt;T&gt;` 相同的生命周期”? (2认同)
  • 相反,我会说类似“`PhantomData` 确保如果`T` 实现`Drop`,那么它将被迫比`Vec&lt;T&gt;` 更长寿,即使存在`#[may_dangle]` `T` 上的属性。” (2认同)

She*_*ter 6

注意事项 - 我在真正回答你问题的极其深刻的理论中并不那么强大.我只是一个曾经使用Rust并且阅读过相关RFC的非专业人士.请务必回顾那些原始来源,以获得更少稀释的真相.


RFC 769引入了实际的Drop-Check规则:

v一些值(临时或命名)和'a一些寿命(范围); 如果v拥有类型数据的类型D,其中(1.) D具有生命周期或类型参数Drop实现,以及(2.)结构D可以达到类型的引用&'a _,以及(3.):

  • (A.)Drop implfor , 直接D实例化,即,或D'aD<'a>

  • (B.)Drop implfor D有一些带有特征限制的类型参数,T其中T一个特征是至少有一个方法,

那么'a必须严格超过范围v.

然后它进一步定义了一些术语,包括一种类型对另一种术语的意义.这进一步PhantomData具体提到:

因此,作为上述标准的另一特殊情况,当类型E拥有类型数据时D,我们包括:

如果EPhantomData<T>,那么递归T.


当同时定义两个变量时,会出现一个关键问题:

struct Noisy<'a>(&'a str);

impl<'a> Drop for Noisy<'a> {
    fn drop(&mut self) { println!("Dropping {}", self.0 )}
}

fn main() -> () {
    let (mut v, s) = (Vec::new(), "hi".to_string());
    let noisy = Noisy(&s);
    v.push(noisy);
}
Run Code Online (Sandbox Code Playgroud)

据我了解,没有Drop-Check规则并指示Vec拥有Noisy,这样的代码可能会编译.当Vec被丢弃,则drop实现可以访问一个无效的参考; 引入不安全.

回到你的观点:

如果我实现Drop一个结构并手动销毁T它中的所有s,为什么我应该关心编译器是否知道我的struct拥有一些Ts?

编译器必须知道您拥有该值,因为您可以/将要调用drop.由于实现drop是任意的,如果您要调用它,编译器必须禁止您接受在丢弃期间会导致不安全行为的值.

永远记住,任意一个T都可以是一个值,一个引用,一个包含引用的值等.当试图弄清楚这些类型的东西时,尝试使用复杂的变体进行任何思考实验都很重要.


所有这一切应该提供足够的连接点; 为了充分理解,阅读RFC几次可能比依赖我有缺陷的解释更好.

然后它变得更复杂.RFC 1238进一步修改了Drop-Check规则,删除了这个特定的推理.它确实说:

参数化是一个必要但不充分的条件来证明dropck所做的推论是正确的

继续使用PhantomData似乎做最安全的事情,但它可能不会是必要的.一位匿名的Twitter赞助商指出了这段代码:

use std::marker::PhantomData;

#[derive(Debug)] struct MyGeneric<T> { x: Option<T> }
#[derive(Debug)] struct MyDropper<T> { x: Option<T> }
#[derive(Debug)] struct MyHiddenDropper<T> { x: *const T }
#[derive(Debug)] struct MyHonestHiddenDropper<T> { x: *const T, boo: PhantomData<T> }

impl<T> Drop for MyDropper<T> { fn drop(&mut self) { } }
impl<T> Drop for MyHiddenDropper<T> { fn drop(&mut self) { } }
impl<T> Drop for MyHonestHiddenDropper<T> { fn drop(&mut self) { } }

fn main() {
    // Does Compile! (magic annotation on destructor)
    {
        let (a, mut b) = (0, vec![]);
        b.push(&a);
    }

    // Does Compile! (no destructor)
    {
        let (a, mut b) = (0, MyGeneric { x: None });
        b.x = Some(&a);
    }

    // Doesn't Compile! (has destructor, no attribute)
    {
        let (a, mut b) = (0, MyDropper { x: None });
        b.x = Some(&a);
    }

    {
        let (a, mut b) = (0, MyHiddenDropper { x: 0 as *const _ });
        b.x = &&a;
    }

    {
        let (a, mut b) = (0, MyHonestHiddenDropper { x: 0 as *const _, boo: PhantomData });
        b.x = &&a;
    }
}
Run Code Online (Sandbox Code Playgroud)

这表明RFC 1238中的更改使编译器更加保守,因此仅使用生命周期或类型参数就足以阻止编译.

您还可以注意到Vec没有此问题,因为它使用unsafe_destructor_blind_to_paramsRFC中描述的属性.

  • 这一切[现在编译](https://play.rust-lang.org/?version=stable&amp;mode=debug&amp;edition=2018&amp;gist=d0a2a38d988dff0ca97ecd8a4a8025d3),我很好奇如何更新解释。 (2认同)