A type that only sometimes capture a lifetime

So essentially something like a Cow, if it's Borrowed it has a lifetime, if it's Owned then it doesn't. My use case is specifically that I have a type that's normally owned, but you can pass it a reference which it will store, thus captures a lifetime.

With safe Rust there is no way to express a type like this without having a lifetime attached to the entirety of the type.

First thing come to mind is to just use two different types, so:

struct Owned(Stuff);
struct Borrowed<'a>(Stuff, &'a OtherStuff);

impl Owned {
    fn store_reference(self, other: &OtherStuff) -> Borrowed<'_>;
impl Borrowed<'_> {
    fn forget_reference(self) -> Owned;

Which works great. Except if the type is stored on heap (reasons could be if you want to erase its type into Box<dyn SomeTrait>, etc.), then this solution adds extra allocations when you convert back and forth.

Is there a way to achieve this without extra allocations?

This is my attempt:


I don't see obvious unsoundness in this attempt, but is this actually sound? Do I have to use unsafe? Is there a better way to do this?

You can use Cow<'static, T> for the case where there's no borrow.


A type such as Box<dyn Capture + '_> also has a lifetime parameter. The only advantage it has over other types with lifetime parameters is that it can utilize some special elision rules that allow you to write Box<dyn Capture> as a shorthand for Box<dyn Capture + 'static>.

As for the code you’re asking about w.r.t. soundness, it looks relatively sound to me, given that user-code will not be allowed to construct arbitrary T values. I can’t think of a way to implement something like release soundly without unsafe, if the requirement is that a Box is involved and re-allocation should be avoided, as there’s no existing API for re-using an allocation of a Box while changing the contained type, even if a completely new value is to be assigned and the type change only changes a lifetime.

However, if avoiding the Box is the goal anyways, then you could just have such API construct a new value and that should not be a problem in safe code. Of course I don’t know your concrete “practical” use-case, so I cannot say for sure how straightforward this would or wouldn’t be.

As for how to solve the problem of using the same type sometimes with and sometimes “without” a lifetime, as hinted at in my first paragraph, all your current solution does is make writing “'static” a bit simpler, and if you can live without that simplification, the straightforward solution is yet to just use something like Cow<'static, T> for the owned lifetime-“free” version. (Though if you want to, you can of course use your own enum, it doesn’t have to be Cow itself.) If writing the 'static becomes too tedious, a type alias can help. E.g. type LifetimeFreeCow<T> = Cow<'static, T>; (good names to be determined).

Of course, this still is a difference at the type level. If you do have a single type Foo<'a> and use Foo<'static> for the owned version, then removing any contained references and converting back to Foo<'static> at the same time will still involve converting e.g. Foo<'a> to Foo<'static> which safe code can only do by re-constructing the struct, and thus if the value was also boxed / on the heap, safe code could do this while re-allocating.

This reminds me, I had previously in some answer in this forum also suggested unsafe code for a “comparable” conversion of making a lifetime 'static safely because all contained references where removed. The use-case was something like Vec<&'a Foo> which was supposed to be re-used in a loop, and each iteration put new local references into the Vec, uses it, and then no longer needs the values, but unsafe was necessary to convert the Vec<&'a Foo> back into Vec<&'static Foo> after .clean()-ing the vec, so it could escape the loop body, and be re-used in the next iteration (then variance could convert it back into a shorter-lived Vec<&'a Foo> again to hold short-lived references). I’d say there might be some general design-work that could be done eventually for coming up with as-general-as-possible safe APIs for re-using allocations (of Vec or Box or other things) while (slightly) changing the type, and where the contained values are dropped / removed / cleared / … to make the type conversion sound. Edit: Found the posts about that in case you’re interested; of course it’s only somewhat relevant for this use-case. Also I mis-remembered, the solution didn’t even involve unsafe code directly but instead relied on specialized implementations for iterators and .collect on Vec. Here’s one comment about such a case and it also links another related discussion.

1 Like

So here’s some potential API for encapsulating re-using a Box, and I’ve re-written the code you posted using safe Rust and that API: Rust Playground

// re-uses the box allocation if possible (i.e. if T and U have the same size and alignment)
// offers a fall-back that re-allocates so code does never rely on any unstable layout
// implementation details in order to compile/run successfully
fn rebox<T, U>(inp: Box<T>, conv: impl FnOnce(T) -> U) -> Box<U> { /* …… */ }

If you compile the code in optimized mode, the assembly is literally identical. (Use the ASM option in the playground to take a look).

<playground::T as playground::Capture>::cap:
	movq	%rdi, %rax
	movq	%rsi, (%rdi)
	leaq	.L__unnamed_5(%rip), %rdx

<playground::T as playground::Capture>::release:
	movq	%rdi, %rax
	movq	$0, (%rdi)
	leaq	.L__unnamed_5(%rip), %rdx

<playground::T as playground::Capture>::read:
	movq	(%rdi), %rax
	testq	%rax, %rax
	je	.LBB11_1
	movl	(%rax), %edx
	movl	$1, %eax

	xorl	%eax, %eax

Thanks for the detailed reply.

I think I titled this post slightly wrong. I should probably have said "A value that ..." instead. Because after all Box<dyn Trait> and Box<dyn Trait + '_> aren't exactly the same type.

So right, my use case can tolerant the type changing with the lifetime capture, as it must do, since capturing a lifetime yet still keep the same 'static type would be unsound. The core of my need is that I have to use a type erased Box, and I want to avoid allocation when the lifetime changes, if possible.

To be more specific. I am trying to provide type erased storage for futures. I have a bunch of different async functions with the same return type. Each of them will be called multiple times, and I want to store the futures with their type erased (since they have the same Output). And I want to not allocate every time I call them, so I am thinking having a preallocated box for each of these function. And that is where the original question comes from, when the box is not holding a future, it doesn't borrow from anything; when it does, then the future will have a lifetime attached to it, so the box must do too.

rebox is really elegant, and it encapsulate the real unsafe-ness of this problem well. But IIUC it would not work with a Pin<Box<T>>? Which would be what I need since I am storing async fn futures. (Maybe with a modification it could?)

Have you seen @alice's reusable-box-future? It doesn't permit storing non-'static futures, but you could probably build on it.

1 Like

BTW, this is the primitive I ended up using:

struct RefHolder<'a, T> {
    inner: Option<&'a T>, // imagine this is !Unpin and invariant w.r.t. 'a
impl<T> RefHolder<'_, T>
    /// Lengthen or shorten the lifetime parameter of the given
    /// `RefHolder`.
    /// # Panics
    /// This function verifies the `inner` field of `Self` is `None`, i.e. `Self`
    /// does not contain any references. If this is not the case, this function
    /// panics.
    fn coerce_lifetime<'a>(self: Pin<Box<Self>>) -> Pin<Box<RefHolder<'a, T>>> {
        // Safety: this is safe because `inner` is `None` and thus does not contain any
        // references. And we do not move `self` out of the `Pin<Box<Self>>`.
        // This is equivalent to Box::pin(RefHolder { inner: self.inner.map(|_| panic!()) })
        // except this doesn't move data or allocate.
        unsafe {
            let raw = Box::into_raw(Pin::into_inner_unchecked(self));
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.