Is `&'a Foo<'a>` a footgun?

&'a Foo<'a> is completely fine to use provided that:

  1. Foo<'a> is covariant in 'a.
  2. You do not intend to obtain a longer-lived reference from the Foo than you can with this definition.

To clarify the second point, consider these two possible structs:

struct One<'slice> {
    foo: &'slice [&'slice str],
}

struct Two<'slice, 'strings> {
    foo: &'slice [&'strings str],
}

If you have Two, you can copy out &'strings str references from it. If you have One, you cannot do that — you can only copy out &'slice strs that are only valid as long as the whole slice reference is.

So, in your original case, if Foo<'a> has no public API by which you can obtain &'a SomethingElse or SomethingElse<'a>, or if Bar never uses Foo in that way, then there is no disadvantage to using &'a Foo<'a>.

You only need two lifetime parameters if you intend to make use of the difference between them, or if there is invariance.

There are also cases where you cannot use two lifetime parameters; in particular, constructing a linked list that points up the stack (or uses an arena, or is in any other way made of references):

struct Node {
    value: String,
    children: Vec<Node>,
}

struct Context<'a> {
    value: &'a str,
    parent: Option<&'a Context<'a>>,
}

fn walk(node: &Node, ctx: Option<&Context<'_>>) {
    let ctx = Context {
        value: &node.value,
        parent: ctx,
    };
    for child in &node.children {
        walk(child, Some(&ctx));
    }
}

If you try to make this code have Context<'a, 'b> (or try to use &mut Context references), you'll find you need Context<'a, 'b, 'c, ...> and so on to infinity — but it works fine with the single lifetime.

5 Likes