Keeping a lifetime internal to a struct

I'm wondering if there is any syntax or pattern for keeping a "subordinate" lifetimes internal to a struct (or another type).

In other words, a way to define a struct like this

struct OuterRefMut<'a> for <'b: 'a> {
    val: &'a mut InnerRefMut<'b>
}

instead of

struct OuterRefMut<'a, 'b> {
    val: &'a mut InnerRefMut<'b>
}

My thinking is that the 'b lifetime needs to outlive the 'a lifetime that's exposed on the struct. So why does 'b need to be part of the struct's parameters (essentially part of its public interface). I can't use the same lifetime parameter because of Borrowing something forever - Learning Rust, not to mention they aren't the same lifetime, one just bounds the other.

Here's some code that runs:

#[derive(Debug)]
struct InnerRefMut<'a> {
    val: &'a mut u64
}

impl<'a> InnerRefMut<'a> {
    fn new(val: &'a mut u64) -> Self {
        Self{val}
    }
}

#[derive(Debug)]
struct OuterRefMut<'a, 'b> {
    val: &'a mut InnerRefMut<'b>
}

impl<'a, 'b: 'a> OuterRefMut<'a, 'b> {
    fn new(val: &'a mut InnerRefMut<'b>) -> Self {
        Self{ val }
    }
}

fn main() {
    let mut val = 0;
    let mut inner_ref = InnerRefMut::new(&mut val);
    let outer_ref = OuterRefMut::new(&mut inner_ref);
    println!("{outer_ref:?}", );
}

Any insights or ideas are appreciated.
Thank you.

This is not possible in Rust currently.

These lifetimes have to exist and be tracked for correctness.

In theory, Rust could add more lifetime elision rules to the syntax to make the 'b not necessary to write out explicitly, but that could make the struct unusable wherever you actually needed to refer to that lifetime, or require more lifetime elision rules elsewhere.

2 Likes

Not directly. You may be able to type-erase the inner lifetime with dyn Trait if it's possible to define the necessary functionality in terms of a trait, and use &'a dyn Trait. You could use a type parameter (in place of one lifetime, or both), technically (but I'm pretty sure you wouldn't be any happier).

Note that the type system isn't just about public API, but is integral to how the compiler works (e.g. borrow-checks). It would be unsound for two different 'bs to result in the same type. Even with covariance it's unsound to extend the inner lifetime.[1] So some hypothetical OuterRefMut<'a> with a hidden 'b would still need the 'b to be part of it's type.

If consumer of your type couldn't tell or dictate that two different OuterRefMut<'a> were really the same type,[2] that would make for a horrible/unworkable coding experience.

What's your XY for wanting to get rid of the second lifetime parameter? Anything beyond aesthetics?


  1. but due to the covariance, &'a InnerRef<'a> can often work when &'a mut InnerRefMut<'a> cannot ↩︎

  2. or allow them to be different ↩︎

1 Like

Thanks for the explanation. That all makes sense. Too much information is lost when a lifetime is expressed as a lifetime parameter.

In other words, the compiler is cool if I turn this:

#[derive(Debug)]
struct OuterRefMut<'a, 'b> {
    val: &'a mut &'b mut u64
}

impl<'a, 'b: 'a> OuterRefMut<'a, 'b> {
    fn new(val: &'a mut &'b mut u64) -> Self {
        Self{ val }
    }
}

fn main() {
    let mut val = 0;
    let mut inner_ref = &mut val;
    let outer_ref = OuterRefMut::new(&mut inner_ref);
    println!("{outer_ref:?}", );
}

into this:

#[derive(Debug)]
struct OuterRefMut<'a> {
    val: &'a mut &'a mut u64
}

impl<'a> OuterRefMut<'a> {
    fn new(val: &'a mut &'a mut u64) -> Self {
        Self{ val }
    }
}

fn main() {
    let mut val = 0;
    let mut inner_ref = &mut val;
    let outer_ref = OuterRefMut::new(&mut inner_ref);
    println!("{outer_ref:?}", );
}

But the inner abstraction removes that flexibility.

What's your XY for wanting to get rid of the second lifetime parameter? Anything beyond aesthetics?

First-order reason, I guess you could call it aesthetics, however, without getting too philosophical, aesthetics are the integration of lots of observations and experiences. They're a heuristic shortcut for what's likely to work and what probably isn't.

So yeah... Every additional lifetime parameter (especially in a trait) is a sharp edge that I'm likely to cut myself on in the future.

Every additional lifetime parameter ... is a sharp edge that I'm likely to cut myself on in the future.

In this case, the future was only a couple hours away. In this case I have a trait that does not have any lifetime parameters, but there is a method to return an OuterRefMut

The trait needs to be implemented on an InnerRefMut and the OuterRefMut can't outlive the mutable borrow of the InnerRefMut. So this would be easily done on an ordinary impl on InnerRefMut, e.g.

impl<'a> InnerRefMut<'a> {
    fn get_outer_ref_mut<'s>(&'s mut self) -> OuterRefMut<'s, 'a> {
        OuterRefMut::new(self)
    }
}

But putting the method into a trait means I don't have the lifetime available in the trait declaration.

trait PublicTrait {
    fn get_outer_ref_mut(&mut self) -> OuterRefMut;
}

impl<'a> PublicTrait for InnerRefMut<'a> {
    fn get_outer_ref_mut(&mut self) -> OuterRefMut {
        OuterRefMut::new(self)
    }
}

// =====Copied from above======

#[derive(Debug)]
struct InnerRefMut<'a> {
    val: &'a mut u64
}

impl<'a> InnerRefMut<'a> {
    fn new(val: &'a mut u64) -> Self {
        Self{val}
    }
}

#[derive(Debug)]
struct OuterRefMut<'a, 'b> {
    val: &'a mut InnerRefMut<'b>
}

impl<'a, 'b: 'a> OuterRefMut<'a, 'b> {
    fn new(val: &'a mut InnerRefMut<'b>) -> Self {
        Self{ val }
    }
}

fn main() {
    let mut val = 0;
    let mut inner_ref = InnerRefMut::new(&mut val);
    let outer_ref = OuterRefMut::new(&mut inner_ref);
    println!("{outer_ref:?}", );
}

You can


  1. You can lose the associated type and just use Self at the cost of dyn capability ↩︎

2 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.