Can you use lifetimes without borrows?

For fun, I'm trying to write a pair of custom pointers, inspired by Rc and Weak.

There's OwnedPtr which maintains exclusive ownership of a piece of data.
And WeakPtr, which lets you immutably access the data, and nothing else.

To avoid dangling, a WeakPtr shouldn't outlive OwnedPtr it was created from, or to abuse some terminology, The WeakPtr's lifetime must be a subset of its OwnedPtr's.

I suspect the answer is just nope™... but is there a way to track an object's lifetime without holding a borrow of it? I'd like for my WeakPtr to be constrained by the lifetime of the OwnedPtr itself, not just a borrow. Plus a borrow existing unnecessarily locks up the borrow checker.

pub struct OwnedPtr<T>(Box<T>);

impl<T> OwnedPtr<T> {
    pub fn new(value: T) -> Self {
        OwnedPtr(Box::new(value))
    }

    // How do I constrain `'a` here?
    pub fn downgrade<'a>(&self) -> WeakPtr<'a, T> {
        WeakPtr {
            data: &*self.0,
            lifetime: std::marker::PhantomData,
        }
    }

    pub fn borrow(&self) -> &T {
        &self.0
    }

    // Safety: Caller must ensure there are no other borrows.
    // They also _probably_ shouldn't mem-swap the underlying data.
    unsafe pub fn borrow_mut(&mut self) -> &mut T {
        &mut self.0
    }
}
pub struct WeakPtr<'a, T> {
    data: *const T,
    lifetime: std::marker::PhantomData<&'a ()>,
}

impl<T> WeakPtr<'_, T> {
    pub fn borrow(&self) -> &T {
        unsafe { &*self.data }
    }
}

The motivation for this is part of larger abstraction for constructing possibly cyclic trees.

Thanks in advance!

You can put the lifetime on the self reference:

pub fn downgrade<'a>(&'a self) -> WeakPtr<'a, T> {

and that's equivalent to the implicit form:

pub fn downgrade(&self) -> WeakPtr<'_, T> {
1 Like

I think you're asking, "how do I name a lifetime 't that lasts until the T is moved/destructed/goes out of scope (but not limited by any borrow)?" If so, there is no direct way to do that.

Generic/inferred lifetimes (those 'a things) and the liveness scope of values (where they are destructed or moved etc) are not the same things. The move or destruction or going-out-of-scope of a value can conflict with (the '_ lifetimes of) borrows related to that value, but those things (moves, destructions, going out of scope) are not themselves lifetimes ('_).

(For these reasons I don't use the phrase "object lifetime" when talking about values going out of scope etc.)


You're not necessarily completely out of luck, but you need some trickery to connect a lifetime to "I know this thing is still around". I think every way I've seen to do it involves some sort of scoped model, where the consumer provides a closure that meets certain requirements. You call the closure and wait for it to return, making sure the thing you need to stay valid can't be moved/destructed/whatever until after the closure returns.

I don't think there's a way without changing up your API significantly.

6 Likes

Thanks for the examples! They seem like pretty interesting reading! I was fully expecting to be completely out of luck, so the fact that it's doable (even if requiring trickery) is actually pretty cool and surprising.

But, I believe you're correct that using a similar strategy would require an entirely new approach : v)

Thanks for the reply! But I was trying to find a way to use lifetimes without using borrows.
Putting the lifetime on the self reference definitely compiles, but it means that OwnedPtr would be treated as borrowed for as long as WeakPtr is alive. Which I was trying to avoid.

I guess my question was accidentally kind of a trick question though, since as I expected (and @quinedot) confirmed, what I was trying to do was (mostly) impossible : v)

I don't get it. That would be unsound. Your weak pointer type borrows from self and returns a related raw pointer. Why would you want it to not be tied to self?

No, that's false. It's not unnecessary, it's required for soundness.

If you want the weak to forcibly and dynamically keep the owned pointer alive, then this is reference counting, and you should be using Rc instead of rolling your own unsafe (and incorrect) memory management.

2 Likes

I guess the question is "is it possible to ensure that value is not dropped, but not restricting its borrowing?" That is, weak pointer should be tied to self without locking self (as the borrow would). OP might correct me if I'm wrong, of course.

2 Likes

Yes, I believe that's also addressed by my last paragraph. It sounds exactly like Rc.

Well, not exactly. Rc is "the data will be dropped once every handle to it is dropped". And this might be intended as "the data will be dropped when owner is dropped, but every other handle must be dropped earlier". This might be implemented with Weak though, I guess, just deferring this last "must" to runtime?

1 Like

What is the practical, functional difference between this and weak.upgrade().unwrap()? That also fails/panics if the weak is dropped earlier than the Rc.

1 Like

Compile-time versus run-time check only, I think. And, yes, in this case it probably can be only runtime-checked.

Hm, I'm not sure I follow. weak.upgrade().unwrap() is runtime-only, isn't it?

Yes, and the question (as I have interpreted it) was "is this the best we can have, or can we shift this check for the compile-time?"

But the answer to "can we do this at compile time" is borrowing. If OP doesn't want neither run-time nor compile-time checking, then what are the remaining options?

And the fact that this is the only answer (that is, that the check they need require some kind of locking) is what the OP might have needed to know.

3 Likes

What I'm trying to emulate is something more like UniqueRc (which is unfortunately unstable), or a version of Rc where strong_count is always guaranteed to be or 1 (or technically 0 while it's in the process of being dropped).

I should probably know better than to plant myself against one of the gods of these forums : vP
But this seems possible. Calling Rc::downgrade returns a Weak, without locking up a borrow of the Rc.

@Cerber-Ursi hit the nail on the head though. My current safety checks are all run-time checks (like Weak), and I was hoping to make them compile-time checks, since unlike Rc, I have a definite source of ownership.
But I wanted to do this in a way that doesn't lock up a borrow (again, mimicking Weak).

From the outset, I suspected this was impossible (and that borrowing was the only answer), but just wanted to make sure before settling for runtime checking : v)

That's because Weak dynamically keeps the allocation alive. The allocation that Rc points to contains a reference count of the outstanding weak pointers as well. This is exactly the same mechanism as strong Rcs themselves use to keep the allocation around. What you assume is a "purely compile-time check" isn't a compile-time check at all, it's a run-time check.

I might not of stated it with sufficient clarity, but I knew that Rc and its family of types used dynamic run-time checks. I was just hoping there was some way to 'upgrade' this to a compile-time check thanks to the additional guarantees my types could uphold. Since that would let me forgo my version of RcBox entirely.

Expectedly, there wasn't, and this is all impossible *shrugs* : v)
Don't get me wrong, run-time checks are fine, I just wanted to make sure that was the best that could be done here.

1 Like

It's not possible to always statically determine when things drop (as in, halting machine not possible). See also:

3 Likes

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.