I'll assume you're after explanations and understanding, versus something practical.
&'lifetime mut T
is covariant over the the lifetime, while parameters in traits such as Foo<'lifetime>
are invariant over the lifetime. See the reference and the nomicon for more details, but the executive summary is that
-
&'long mut T
can coerce into a &'short mut T
if need be, but
-
A: Foo<'long>
does not imply A: Foo<'short>
(or A: Foo<'longer>
)
So with that in mind, let's look at your trait-with-generics.
trait Foo<'a> {
fn foo(bar: &'a mut Bar) -> Self;
}
Each implementation of this trait let's me call foo
with a borrow of lifetime 'a
-- and no other lifetime, only 'a
will do. If my implementation is generic, it might cover all possible lifetimes:
struct S;
impl<'some_lifetime> Foo<'some_lifetime> for S {
fn foo(bar: &'some_lifetime mut Bar) -> Self {
S
}
}
But the implementation is distinct for every lifetime. That is, you can only call foo(&'x mut Bar)
if Foo<'x>
specifically is implemented.
OK, but how about using it?
impl<'a, A, B> Foo<'a> for (A, B)
where
A: Foo<'a>,
B: Foo<'a>,
Within this implementation, you can assume your bounds have been met -- but nothing more. And your bounds say that you only have one specific lifetime available -- 'a
-- and that A
and B
implement Foo<'a>
for that one specific lifetime. You cannot assume they implement Foo<'shorter_than_a>
(or 'longer
) due to the invariance of the trait parameter.
So then in the implementation body:
fn foo(bar: &'a mut Bar) -> Self {
(A::foo(bar), B::foo(bar))
The only foo
s you can call are
<A as Foo<'a>>::foo(&'a mut Bar)
<B as Foo<'a>>::foo(&'a mut Bar)
Because that's all that is permitted by your bounds. You only know Foo<'a>
is implemented, so you can only call foo
with a lifetime of 'a
, as per the trait definition. These two borrows would clearly overlap -- they're exactly the same -- and that is not permitted (aliasing a &mut
is instant UB).
Why does the version without generics work?
trait Foo {
fn foo(bar: &mut Bar) -> Self;
// Same as:
// fn foo<'any>(bar: &'any mut Bar) -> Self;
}
The lifetime in foo
is no longer tied to a trait parameter. In order to implement this trait, you have to be able to write a foo
that takes a &mut Bar
of any lifetime, not just one specific lifetime. So when it comes to the tuple implementation:
impl<A: Foo, B: Foo> Foo for (A, B) {
fn foo(bar: &mut Bar) -> Self {
(A::foo(bar), B::foo(bar))
}
}
We can make it a lot more verbose like so:
fn foo<'x>(bar: &'x mut Bar) -> Self {
let a = <A as Foo>::foo(bar); // Reborrow 1
let b = <B as Foo>::foo(bar); // Reborrow 2
(a, b)
}
We get a &'x mut Bar
, which is covariant over 'x
. So when we call A::foo
, we can reborrow bar
as a &'very_short mut Bar
. The reborrow ends as soon as A::foo
returns. Something similar happens with B::foo
. Due to these short reborrows, there is no overlap. The calls themselves are valid because to implement Foo
, your foo
function must accept any lifetime; we don't have to call them with 'x
specifically. We call each with something shorter, as short as the function call itself.
The &'a mut Bar
was covariant in the generic example too, but that didn't help us because we had to call the foo
functions with a lifetime 'a
. That's all the bounds allowed; we couldn't use anything shorter.
I assumed you knew about reborrows there. I wish I had a good reference for reborrows, but the official documentation on them is poor. Basically when you reborrow something:
let reborrow: &mut Bar = &mut *bar;
It's not an overlap because this is reusing the original borrow, sort of like you can do:
let data_structure: &mut Data = todo!();
let field: &mut String = &mut data_structure.field;
Because the lifetime is covariant, the reborrow can have a shorter lifetime. Once your reborrow expires, you can use the original again.
Rust inserts reborrows automatically in some places, and the above example is one of them.
So now that we see how the original worked, can we "fix" the generic version?
Yes, easily! Remove the lifetime from the trait!
Okay, okay, let's try something else. The problem was that we could only call A::foo
and B::foo
with the same lifetime. What if we could call them with any lifetime, similar to how it worked without generics?
impl<'a, A, B> Foo<'a> for (A, B)
where
- A: Foo<'a>,
- B: Foo<'a>,
+ A: for<'any> Foo<'any>,
+ B: for<'any> Foo<'any>,
That works. These new bounds say that A
and B
must implement Foo<'_>
for all possible lifetimes, and that means that we can call A::foo
and B::foo
with any lifetime, just like the non-generic case. (This shows more directly that the &'a mut Bar
is still covariant -- we can reborrow shorter lifetimes from it.)
Those bounds are called higher-ranked trait bounds (HRTBs).