Just another GAT + lifetimes problem

I've got into some weird GAT lifetimes error and hope someone can help me here.

At first some requirements:

  • Context comes from outside, I cannot control it and it can be non-'static.
  • Context defines a Node which it can create.
  • Context lifetime is not bound to Node and it can dropped at any time (or live longer).
  • Node has a generic lifetime, so outside code can make them reference each other
  • this tree of Node references is always one-way (non-cyclic) and I can enforce it (not part of the example)

For this to work I need to return &'a Node<'a> (all nodes have the same lifetime and can reference each other).

But it fails to compile with some strange error related to Ctx not being alive long enough.
Even more strange is that it is related to lifetime I return &'a. If I remove it - everything works. But i can't make a references tree.

Everything works if I remove "G" from GAT (lifetime from Node) but I need that "G".

So the question is why on earth does this &'a bounds Ctx lifetime and how to fix it.

PS: I know about using indexes but it doesn't fit my design goal. I'd like to make everything work by using references and lifetimes only (no vec[index], Rc, Arc, etc.).

Code:

trait Context {
    type Node<'a>;

    fn new_node<'a>(&mut self) -> Self::Node<'a>;
}

struct Storage<'a> {
    vec: Vec<NonNull<()>>,
    phantom: PhantomData<&'a mut ()>,
}

impl<'a> Storage<'a> {
    fn allocate<Ctx: Context>(
        &mut self,
        context: &mut Ctx,
    //    vv removing this 'a "fixes" the problem
    ) -> &'a mut Ctx::Node<'a> {
        // dummy placeholder implementation here
        let mut ptr = NonNull::from_ref(Box::leak(Box::new(context.new_node())));
        self.vec.push(ptr.cast());
        unsafe { ptr.as_mut() }
    }
}

fn new_node<Ctx>(context: &mut Ctx, storage: &mut Storage)
where
    Ctx: Context,
{
    storage.allocate(context); // error here
}

Error:

error[E0311]: the parameter type `Ctx` may not live long enough
    |
141 | pub fn new_node<Ctx>(context: &mut Ctx, storage: &mut Storage)
    |                                                       ------- the parameter type `Ctx` must be valid for the anonymous lifetime defined here...
...
145 |     storage.allocate(context);
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^ ...so that the type `Ctx` will meet its required lifetime bounds
    |
help: consider adding an explicit lifetime bound
    |
141 ~ pub fn new_node<'a, Ctx>(context: &mut Ctx, storage: &mut Storage<'a>)
142 | where
143 ~     Ctx: Context + 'a,
    |

&'a mut Node<'a> in rust is the infamous "borrowing forever" anti-pattern. exclusive references (a.k.a. mut borrows) are invariant over the type, meaning the compiler cannot shorten the lifetime parameter of the type, resulting a self-borrowing situation that lasts forever, see:

for tree like data structure with arena allocated nodes, it is common to use Cell<Option<&'a Node<'a>>> for internal pointers.

shared references are covariant over the type, and they are also Copy, so you don't have the borrowing forever problem, and Cell is used to achieve shared mutability (a.k.a. interior mutability).

1 Like

Nice, I have one more thing to remind myself about &'a mut Node<'a> ...

But in any case this doesn't help. Removing mut leads to the same error:

fn allocate<Ctx: Context>(&mut self, context: &mut Ctx) -> &'a Ctx::Node<'a> {
    ...
}

Error:

error[E0311]: the parameter type `Ctx` may not live long enough
    |
141 | pub fn new_node<Ctx>(context: &mut Ctx, storage: &mut Storage)
    |                                                       ------- the parameter type `Ctx` must be valid for the anonymous lifetime defined here...
...
145 |     storage.allocate(context);
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^ ...so that the type `Ctx` will meet its required lifetime bounds
    |
help: consider adding an explicit lifetime bound
    |
141 ~ pub fn new_node<'a, Ctx>(context: &mut Ctx, storage: &mut Storage<'a>)
142 | where
143 ~     Ctx: Context + 'a,
    |

You haven't specified what lifetime the Storage argument has, so you get an implicit default. It should be Storage<'a>. The latest version of Rust warns about this footgun by default.

The error message you're getting is right

Where should I specify it? Just adding 'a doesn't work. I've even tried adding them everywhere, still no success:

pub fn new_node<'a, 'b, 'c, Ctx>(context: &'c mut Ctx, storage: &'b mut Storage<'a>)
where
    Ctx: Context,
{
    storage.allocate(context);
}

Adding Ctx: Context + 'a is a no go for me.

This is much more closer example to what I have in the real project:

trait Context {
    type Node<'a>;

    fn new_node<'a>(&mut self) -> Self::Node<'a>;
}

struct MyScope<'context, 'storage, Ctx> {
    context: &'context mut Ctx,
    storage: &'storage mut Vec<()>,
}

impl<'context, 'storage, Ctx> MyScope<'context, 'storage, Ctx> {
    fn new_node_scope_ctx<F>(&mut self, f: F)
    where
        Ctx: Context,
        F: FnOnce(&mut Ctx::Node<'storage>),
    {
        self.new_node_scope_any(f)
    }

    fn new_node_scope_any<Node, F>(&mut self, f: F)
    where
        Node: 'storage, // removing this makes code compile but I need that lifetime
        F: FnOnce(&mut Node),
    {
        todo!()
    }
}

Same error:

error[E0309]: the parameter type `Ctx` may not live long enough
    |
162 | impl<'context, 'storage, Ctx> MyScope<'context, 'storage, Ctx> {
    |                -------- the parameter type `Ctx` must be valid for the lifetime `'storage` as defined here...
...
168 |         self.new_node_scope_any(f)
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^ ...so that the type `Ctx` will meet its required lifetime bounds
    |
help: consider adding an explicit lifetime bound
    |
162 | impl<'context, 'storage, Ctx: 'storage> MyScope<'context, 'storage, Ctx> {
    |                             ++++++++++

Why is Ctx becomes bound to 'storage lifetime? I'm not even using it with that lifetime, only GAT provided by it.

Found the solution, I just had to add 'a lifetime to the GAT itself:

trait Context {
    type Node<'a>: 'a; // <-- here
}

Now I feel bad for not thinking about it before, it should have been so obvious...

That just tells me that the diagnostic is bad. :slight_smile:

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.