Match on enum where a variant holds a cow'ed slice

Hello everyone,

While working on a personal project, I ended up fighting the borrow checker for a piece a code that should be totally fine.
The problem is about cloning a struct where a field is an enum where one variant holds a Cow of a u8 slice:

#[derive(Debug)]
enum TestEnum<'a> {
    Variant1,
    Variant2(Cow<'a, [u8]>),
    Variant3,
}

#[derive(Debug)]
struct TestStruct<'a> {
    e: TestEnum<'a>,
}

In all the various examples i tried, i noticed that the only way to please the borrow checker is to manually enumerate all the variants in the match statement. If you use a fallback _ or combine various variants in one arm, the borrow checker complains.
Does not work:

impl<'a> TestStruct<'a> {
    fn into_owned<'b>(self) -> TestStruct<'b> {
        let Self {
            e,
        } = self;
        match e {
            TestEnum::Variant2(data) => TestStruct {
                e: TestEnum::Variant2(data.into_owned().into()),
            },
            e @ _ => TestStruct {
                e,
            },
        }
    }
}

Does not work:

impl<'a> TestStruct<'a> {
    fn into_owned<'b>(self) -> TestStruct<'b> {
        let Self {
            e,
        } = self;
        match e {
            TestEnum::Variant2(data) => TestStruct {
                e: TestEnum::Variant2(data.into_owned().into()),
            },
            e @ TestEnum::Variant1 | e @ TestEnum::Variant3 => TestStruct {
                e,
            },
        }
    }
}

Does work:

impl<'a> TestStruct<'a> {
    fn into_owned<'b>(self) -> TestStruct<'b> {
        let Self {
            e,
        } = self;
        match e {
            TestEnum::Variant2(data) => TestStruct {
                e: TestEnum::Variant2(data.into_owned().into()),
            },
            TestEnum::Variant1 => TestStruct {
                e: TestEnum::Variant1,
            },
            TestEnum::Variant3 => TestStruct {
                e: TestEnum::Variant3,
            },
        }
    }
}

I would like to understand better the problem the borrow checker is facing that forbids me to write code like the first example, as that is ideally what i want. Or find a way to not have to enumerate all the variants as in my real project as i have many.

Thank you in advance for your help / explanations!

It's because the compiler doesn't realize the other variants have no references when using a default case. For example, if you say that the result still has a reference with the same lifetime, it's okay:

impl<'a> TestStruct<'a> {
    fn into_owned(self) -> TestStruct<'a> {
        match self.e {
            TestEnum::Variant2(data) => TestStruct {
                e: TestEnum::Variant2(data.into_owned().into()),
            },
            e => TestStruct {
                e,
            },
        }
    }
}

but if you want an owned version with a static lifetime, you have to explicitly go through them to allow the compiler to see you're not actually doing shenaniganz with lifetimes:

impl<'a> TestStruct<'a> {
    fn into_owned(self) -> TestStruct<'static> {
        use TestEnum::*;
        match self.e {
            Variant2(data) => TestStruct {
                e: Variant2(data.into_owned().into()),
            },
            Variant1 => TestStruct {
                e: Variant1,
            },
            Variant3 => TestStruct {
                e: Variant3,
            },
        }
    }
}
1 Like

Part of the problem because self's lifetime is elided the compiler will choose 'a and there isn't enough other information to let the compiler know that 'b is shorter.
notice if changed to

impl<'a> TestStruct<'a> {
    fn into_owned<'b>(self) -> TestStruct<'b> 
    where
        'a: 'b
    {
        let Self {
            e,
        } = self;
        match e {
            TestEnum::Variant2(data) => TestStruct {
                e: TestEnum::Variant2(data.into_owned().into()),
            },
            e @ TestEnum::Variant1 | e @ TestEnum::Variant3 => TestStruct {
                e,
            },
        }
    }
}

it compiles. As @alice said you either make everthing 'a or add enough information so the compiler doesn't need to make guesses.

2 Likes

Regarding the generic 'b lifetime, that's equivalent to returning a 'static lifetime because the actual value of a generic is chosen by the caller. This means I can choose 'b = 'static when calling it to get a version with a 'static lifetime, and since 'static is the strongest lifetime, using an arbitrary generic is the same as just using 'static.

On the other hand, if you add a where 'a: 'b bound as in @DevinR528's comment, it's equivalent to an 'a, since the caller can choose any generic they want, as long as it's shorter than 'a.

1 Like

Thank you for your answers
Regarding:

In this situation, the compiler could be improved to understand that the other variants do not carry any lifetime? By lets say, create a virtual enum with only the remaining variants or some other tricks. Is there any actual github issue that could tracks this kind of improvements?

About:

as Alice pointed out, it's not something that i want because i want to return an owned value to the caller for the lifetime of his choosing.

There's one thing you haven't tried that reveals quite a bit more about the nature of the problem:

        match e {
            TestEnum::Variant2(data) => TestStruct {
                e: TestEnum::Variant2(data.into_owned().into()),
            },
            e @ TestEnum::Variant1 => TestStruct { e },
            e @ TestEnum::Variant3 => TestStruct { e },
        }

This also fails. You see, the issue is very specifically with the creation and use of the e binding. Because the input to match is TestEnum<'a> and e matches the entire input, e accordingly has type TestEnum<'a>, and thus cannot be used as a TestEnum<'b>.

Call it a compiler limitation.

The third example compiles because you are deconstructing e and producing a brand new TestEnum (via the expression TestEnum::Variant1) with no existing lifetime constraints.

3 Likes

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