Any way to guarantee reference to ZST in PhantomData is zero-cost

I have two types that look like this:

pub type Foo<'a> = PhantomData<fn(&'a ()) -> &'a ()>;
pub struct Bar<'a>(PhantomData<&'a Foo<'a>>);

How can I construct a Bar and with the same lifetime as a Foo and be guaranteed constructing Bar has zero overhead?

One way to do it is like this:

impl<'a> Bar<'a> {
    pub fn new(_: &'a Foo<'a>) -> Self {
        Bar(PhantomData)
    }
}
let foo: Foo = PhantomData;
let bar = Bar::new(&foo);

But this is no longer guaranteed to be zero-cost, as I create a reference to a ZST which is not guaranteed to be zero sized or optimized away. And changing the constructor to take in a Foo<'a> without a reference changes the behavior of my program. I feel like there should be a way to do this without actually creating the reference, or am I asking for too much and is this not possible?

Well, what behavior do you care about preserving here? Incidentally this

// n.b. `Foo<'x>` is invariant in `'x`
pub fn new(_: &'a Foo<'a>) -> Self 

share-borrows the Foo<'a> forever. Is that intentional and the behavior you're trying to preserve? Or perhaps construction of Foo controlled (no Copy/Clone), or is it something else.

Another question that may or may not matter: what's the definition of Id<'_>?


Program semantics aside, I wouldn't worry about the optimization costs of deciding to use the reference passing approach unless measurements indicated this was actually a performance problem.

1 Like

I don't understand why you think there is overhead. But if you really don't want to pass a reference, this also might work:

impl<'a> Bar<'a> {
    pub fn new(_: Foo<'a>) -> Self {
        Bar(PhantomData)
    }
}

The word "guarantee" doesn't normally apply to optimizations, so I suggest not to focus on that. There are very few that are absolutely guaranteed, because the optimizer can change, have bugs, etc.

1 Like

Yes, that is intentional. Right now I'm trying to prove that generativity is zero-cost (for fun) and I'm just experimenting with it. It's not a specific case of saving performance in which I would trust the compiler to do so.

I'm sorry, that was supposed to be Foo (I edited the post)

I'm not very sure why but doing so causes the following previous-correctly-rejected program to compile. Rust Playground

It doesn't compile because each invariant lifetime's drop time is "wedged" in between foo_[n] and bar_[n] thus making them ununifiable. But removing the reference makes this no longer the case?

I'm sorry, I'm of no help on that.

It's related to the borrowed-forever aspect I mentioned earlier, and how they're incompatible with non-trivial destructors (although the destructor in question has now been shifted elsewhere).

When you take a reference &'a Foo<'a>, the return value (a Bar) is considered to be able to observe the borrow of Foo<'a> when it drops. You do this with both bar_1 and bar_2 and then unify the lifetimes, so both are considered to be able to observe foo_1 and foo_2 when they drop. bar_1 drops after foo_2 and triggers a borrow checker error. But if both bars drop before the foos instead, it compiles.

When you remove the reference from Bar::new, you are now moving the Foos into new, not borrowing them. When the bars are destructed, they may still be considered to be able to "observe the lifetime", but the lifetime isn't associated with an outstanding borrow and there is no borrow checker conflict.

So probably you need the reference anyway, but also, there probably is no tangible cost to worry about.

1 Like

Thanks for the explanation. So it looks like the reference is necessary. Allow me to rephrase my question to ask if there is any other possible way to create Bar without creating a reference to Foo anywhere; the type itself hides this away in a PhantomData, so I would think that this shouldn't be needed. You're right in that this is overwhelmingly likely in practice, but I was just curious if there was a way create a Bar that is provably zero-cost using the available guarantees Rust currently provides.

Well, the challenge isn't creating a Bar<'a>, it's creating Bar<'a> that can't be unified with other Bar<'b>. In the examples we see that is done not only by passing a reference, but by controlling the drop order via declaration order (which in the crate, is only ensured when you use the macro -- or, as a defensive measure, unsafe (in which case any UB is on you)).

That is your real question as far as I can tell -- is there a way to create "unique lifetimes" without closure scopes, and also without creating references / only using ZSTs, or such.

I'm not sure if there is or not. I'm not thinking of one off-hand, but it could be a neat exercise to attempt.

I don't know your mental model so I'm not sure where that intuition comes from. For me, I see that a core mechanism of the borrow-checker enforced approach is to create a borrow observed in a drop. Remove the reference and you remove the borrow.

I haven't thought of another way to entangle the Bar with a borrow.

What available guarantees? I am unaware of any currently provided guarantees that not creating a reference is going to be more performant than creating a reference as it pertains to this topic. As was noted:

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.