Is there any reason to prefer a boxed slice of references over a reference to a slice of references?

struct A<'a> {
    a: &'a str,
    b: u64,
    c: u64
}

struct B<'a> {
    d: &'a [&'a A<'a>]
}
// OR
struct C<'a> {
    d: Box<[&'a A<'a>]>
}

Is there anything I can do with B that I can't do with C or that I can do with C and can't do with B?

C's slice is owned while B's is borrowed. The exact same benefits and downsides apply as with regular owned vs borrowed data (e.g. &str vs String):

  • Borrowed data must be owned by someone else.
  • Borrowed data cannot be returned.
  • Borrowed data cannot be stored in a type that exists for longer than one scope.
  • Owned data incurs a heap allocation.
  • Owned data can be mutated, shared borrowed data can't be.

But in this case they both contain borrowed data. So neither the boxed slice nor the referenced slice would be able to exist without the content being owned by something else

Beyond the borrowed/owned distinction, two other facts spring to mind:

  1. B is Copy, C is not.

  2. B unifies 3 lifetimes, whereas C unifies only 2. This means there might be some things you can do with C that you can't do with B, but you could do with

    struct D<'a, 'b> {
        d: &'b [&'a A<'a>],
    }
    

True, but (you've obfuscated this by using the same lifetime for both) the slice doesn't have to be borrowed from the same place as the borrows inside the slice.

3 Likes

Concretely:

// can be implemented for B<'a>, but not C<'a>
trait Sneep<'a> {
    fn sneep(&self) -> &'a &'a A<'a>;
}

// can be implemented for C<'a>, but not B<'a>
trait Darb<'a> {
    fn darb(from: &'a A<'a>) -> Self;
}
1 Like

Thank you very much !

Oh and btw, are there any performance tradeoffs for B or C ?

If the contents are already in a slice somewhere, then creating a B is essentially free (since it is just a reference to that existing slice), while creating a C is more expensive since it requires allocating a new slice, copying all the contents into it, and also deallocating the slice when the C is dropped.

On the other hand, if you are creating a separate vector (or other owned slice) for each B that you create, then you may end up paying the same costs overall. The cost of B is cheaper mainly in the case where the B is shorter-lived than the data it points to, or where you have many B referencing the same underlying data.

Hm ok thanks !