Intuition on why Cows of lifetimed structs are incompatible

Consider the code here where I define two functions which each take an argument a : Cow<'short, S<'long>> and an argument b : Cow<'short, S<'short>>, where 'long outlives 'short, and S is a type parametric in a lifetime. One of both functions assigns a = b, the other assigns b = a. Both functions are rejected by the borrow checker.

When I try to reason about the reason for the failed borrowcheck, I always end up feeling that one of them is indeed bad, but that the other one seems valid. When I start reasoning about the other, I suddenly feel like the first one can't actually go wrong.

Can any of you help me get a proper intuition about why both cases are potentially bad?

(As a bonus, the error messages are super-confusing, as they read "these two types are declared with different lifetimes", "...but data from a flows into a here" which sound like a perfectly fine assignment.)

This is ultimately a case where the compiler rejects valid code. To see this, consider the equivalent code:

fn f1<'long, 'short>(mut a: Cow<'short, S<'long>>, mut b: Cow<'short, S<'short>>)
where
    'long: 'short,
{
    match a {
        Cow::Borrowed(a) => {
            b = Cow::Borrowed(a);
        },
        Cow::Owned(a) => {
            b = Cow::Owned(a);
        },
    }
}

which compiles. What happens here is that the owned variant is defined as

Owned(<B as ToOwned>::Owned)

and this makes the Cow type invariant in T because no special handling has been implemented to check that in this case, the associated type doesn't cause trouble when the lifetime is shortened.

3 Likes

Ah, I naively assumed covariance. That explanation makes a lot of sense, thanks.

I can't seem to turn your workaround into a workable solution for my problem, though. In retrospect, in an attempt to have a minimal representative example, I have oversimplified. A more representative example would have struct S looks like this:

struct S<'l> {
   field: HashMap<String, S<'l>>,
   unrelated_fields___ i32,
}

Here, the proposed workaround does not really work: I shouldn't have to deconstruct arbitrarily many levels of HashMap<_, S<'long>> and move the data over into new HashMap<_, S<'short>>s, to end up with the exact same data structure. At first sight, the sledgehammer approach works:

let long: Cow<'short, S<'long>> = get_it_somewhere();
let short: Cow<'short, S<'short>> = core::mem::transmute(long);

but I'm not 100% sure the transmute is sound, and — related to that — I prefer to keep as many of Rust's checks in place since rustc is better at checking soundness than I am.

Is that transmute

  1. an option, and
  2. probably the best available option?

Do some of your unrelated fields cause S<'l> to be invariant in 'l? Because it seems like it is covariant. If it is, it seems you could make your own safe transmute:

fn cow_transmute<'l: 's, 's>(a: Cow<'s, S<'l>>) -> Cow<'s, S<'s>> {
    match a {
        Cow::Borrowed(a) => Cow::Borrowed(a),
        Cow::Owned(a) => Cow::Owned(a),
    }
}

playground

1 Like

Oh my... :flushed: I swear it doesn't work in the full context of my codebase... :grimacing:

I'll dissect the issue some more on my own. You definitely helped me understand the kind of problem that the error message indicates, which is a big step in the right direction. Thanks for your persistence.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.