Dealing with references to references


#1

Hi Often times, I have the following pattern in code:

// A struct which owns some data and some references
struct Analysis<'f> {
    cache: HashMap<&'f str, u64>
}

// A struct, which borrows Analysis
struct Codegen<'a, 'f: 'a> { // Note: I have to declare two lifetimes here
    analysis: &'a Analysis<'f>, // This is what bothers me!
    node_types: Vec<(Text<'f>, bool)>,
}

That is, I want to store a field of type Foo, which also has a lifetime. So the field has two lifeimes: &'a Foo<'b>, and I have to declare both lifetimes for the containing struct. Is it possible some how to parameterize the struct over a single lifetime instead? I would love something like this:

struct Codegen<'a> {
    analysis: &'a Analysis<???>,
    node_types: Vec<(Text<???>, bool)>,
}

#2

If Analysis and Text are variant, you should be able to use a single lifetime for all those lifetimes there. I think the multiple lifetimes (with explicit outlives relationship specified) are needed if the types are invariant.


#3

I’ve found an interesting workaround: it is possible to hide the inner lifetime behind a trait object. https://matklad.github.io/2018/05/04/encapsulating-lifetime-of-the-field.html


#4

Yeah, a trait object can be a suitable technique here. You can also make the type generic over the trait and avoid the virtual dispatch overhead. This does add a generic type parameter, but it hides the lifetime parameter(s) of the underlying type:

struct Foo<'a> {
    buff: &'a  mut String
}

impl<'a> Foo<'a> {
    fn push(&mut self, c: char) {
        self.buff.push(c)
    }
}

trait Push {
    fn push(&mut self, c: char);
}

impl<'a> Push for Foo<'a> {
    fn push(&mut self, c: char) {
        self.push(c)
    }
}

struct Context<'f, P: 'f> {
    foo: &'f mut P,
}

impl<'f, P: Push> Context<'f, P> {
    fn new(foo: &'f mut P) -> Self {
        Self { foo }
    }

    fn push(&mut self, c: char) {
        self.foo.push(c)
    }
}

fn test<'f, 'a>(foo: &'f mut Foo<'a>) {
    let mut ctx = Context::new(foo);
    ctx.push('9');
}

Type inference typically makes the use sites fairly concise since the exact type are inferred, but that’s also the case with generic lifetime parameters. So I don’t know if you think this is an improvement or not.

You could go even further and abstract over an owned or borrowed Foo, which adds another type parameter to Context but removes the lifetime parameter.