PhantomData<T> has no effect in practice?

hello, I come in a understanding of PhantomData which I know I am wrong, but I can't find a proof to show where is the error: I get a conclusion: PhantomData has no effect in practice. Anybody can explain why PhantomData is useful?

Let me introduce the theory:

At first, let's talk without #[may_dangle], It is unstable and may been moved in the future. Since PhantomData is stable, It should have meanings without it.

Ok, there is a struct like this:

pub struct Box<T: fmt::Debug> {
    ptr: *mut T,
    _marker: marker::PhantomData<T>
}

then what is the effect with _marker?

In general, it is said the effect is making rustc believe Box owning the T.

But that effect does not force rustc to call drop_in_place(ptr) automatically, author of Box still has to call it by hand --- same as without _marker. If the author forget the call, rustc will not report, even with _marker being present.

If it is not for the author to force correctness, then it is for the user of Box?

While, if it has some effect, the effect should be disallow the user dropping T because the author will dropping it.

Ok, let's examine how the _marker is initialized. Obviously, it initialized via ptr, while, ptr is initialized with either borrow or move.

If it is from borrow, then the ownership is still held in user side; and Box can't get the ownership from a borrow. If boxes author want to get the ownership for _marker, he will get a compiling error: it is just the general rules taking effect.

If it is from moved, then the user side does not hold the ownership anymore, then the only drop take place in box, e.g. user side will not try to drop T, and _marker is useless because there is never a place for it to show itself useful.

Finally, I succeed writing some code to make rust believing both Box and user side owning the T, which I expected to fail to be compiled.
But in fact, it passed the compiling --- the with/without _marker does not show a difference.

#[repr(transparent)]
#[derive(Debug)]
struct PrintOnDrop<T : fmt::Debug> (T);
impl<T : fmt::Debug> Drop for PrintOnDrop<T> { fn drop(&mut self) {
    if mem::needs_drop::<T>() {
        eprintln!("PrintOnDrop: dropping {0:?} at {0:p}", self);
    } else {
        eprintln!("PrintOnDrop: no dropping fields of {0:p}", self);
    }
}}

fn case1() {
    let s = String::from("Hello, World!");
    let obj = PrintOnDrop(&s);

    let mut p = Box::new(None);
    let mut y = Some(obj);
    p.ptr = &mut y;

    dbg!(p.get());
}

Playground

Have you read PhantomData in std::marker - Rust and/or PhantomData - The Rustonomicon ? For example:

PhantomData consumes no space, but simulates a field of the given type for the purpose of static analysis. This was deemed to be less error-prone than explicitly telling the type-system the kind of variance that you want ...

Any answer we could type out for you would essentially be a duplicate of these excellent resources.

Yes I have read, but no answers.
It is raised just when try to understand the documents above

Well, the direct answer to your question is exactly what they say: PhantomData doesn't "do" anything at runtime, nor does it have any effect on ownership/borrow-checking. It only tells Rust what variance your type should have over its type and lifetime parameters, if any (and helps with the corner case called drop check). That's all.

Ok, then _marker in

pub struct Box<T: fmt::Debug> {
    ptr: *mut T,
    _marker: marker::PhantomData<T>
}

can be deleted?

If not, what is the corner case of drop check?
If it is unnecessary, then how can I make it necessary to cover the corner case of drop check?

Drop Check - The Rustonomicon covers drop check, and PhantomData - The Rustonomicon explains how PhantomData helps with drop checking. I believe you would need a PhantomData in your Box<T> for the same reason the Vec<T> does in that page's
example: it tells the drop checker that Box<T>/Vec<T>'s drop() impl might drop a T, and it needs to know that to make Box<T>/Vec<T> compose with other generic code soundly.

2 Likes

That is what the document says:

In order to tell dropck that we do own values of type T, and therefore may drop some T's when we drop, we must add an extra PhantomData saying exactly that:

use std::marker;

struct Vec {
data: *const T, // *const for variance!
len: usize,
cap: usize,
_marker: marker::PhantomData,
}

And it is my confusion:
What is the difference with _marker: marker::PhantomData being present, if it has some effect, what the effect is, If I delete it, where bugs come from?

In order to tell dropck that we do own values of type T

in the playground, I do tell dropck I own T, and indeed the user code holds it too.

What I expect is a compiling error, but it compiles successfully.

On further inspection, I also cannot produce a case where drop check requires PhantomData for soundness, and apparently this is a known issue where the Nomicon is likely out of date: Rework drop check stuff, it's probably outdated · Issue #17 · rust-lang/nomicon · GitHub

There's no example for how this could be done and I couldn't construct one myself.

So maybe it is just for variance these days.

1 Like

Without the marker it compiles and a dropped object is accessed.
With the marker it is correctly prevented for compiling.
Removing the #[may_dangle] also prevents it form compiling.

So it most likely only matters to dropck if #[may_dangle] is used.

4 Likes

I'm just going to link to @Yandros's amazing explanation of PhantomData

5 Likes

Moral of the story: "Rust's program analysis is much more complex and sophisticated than first appears."

4 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.