I can't understand why a mutable borrowed variable is still borrowed after the scope of the borrower ends. It looks like it is related to trait usage, but I don't see why:
fn main() {
let mut a = 10;
test::<FooS>(&mut a);
println!("out {:?}", a)
}
trait Foo<'a> {
fn new(data: &'a mut u32) -> Self;
fn apply(&mut self);
}
struct FooS<'a> {
data: &'a mut u32,
}
impl<'a> Foo<'a> for FooS<'a> {
fn new(data: &'a mut u32) -> Self {
FooS { data: data }
}
fn apply(&mut self) {
*self.data += 10;
}
}
fn test<'a, F>(data: &'a mut u32)
where F: Foo<'a>
{
{
// let mut foo = FooS {data: data}; // This works fine
let mut foo: F = Foo::new(data);
foo.apply();
} // foo scope ends here
println!("{:?}", data); // error
} // but borrowed till here
try online
error: cannot borrow `data` as immutable because `*data` is also borrowed as mutable [--explain E0502]
--> <anon>:34:22
31 |> let mut foo: F = Foo::new(data);
|> ---- mutable borrow occurs here
...
34 |> println!("{:?}", data); // error
|> ^^^^ immutable borrow occurs here
35 |> } // but borrowed till here
|> - mutable borrow ends here
The test
function requires that type F
implements Foo<'a>
. The 'a
there is a lifetime parameter that's passed to the function. Lifetime parameters always represent lifetimes that live longer than the function call – because there is just no way a caller could supply a reference with a shorter lifetime; how could you pass a reference to a local variable from another function? –, and for the purposes of borrow checking (which is local to a function), the compiler considers that the borrow covers the whole function call.
Therefore, when you create an instance of F
from the call to Foo::new
, you create an object that borrows something with lifetime 'a
, a lifetime that covers the whole function call.
It's important to understand that when you call test::<FooS>
, the compiler actually fills in a lifetime parameter for FooS<'a>
, so you end up calling test::<FooS<'a>>
, where 'a
is the region that covers the statement that contains the function call (because &mut a
is a temporary expression). Therefore, the compiler thinks that the FooS
that would be constructed in test
would borrow something until the end of the statement with the call to test
!
Let's contrast this with the nongeneric version:
let mut foo = FooS {data: data};
In this version, the compiler chooses a concrete lifetime for FooS<'a>
in test
, rather than in main
, so it will choose the block suffix extending from the end of the let
statement to the end of the block, which means that the next borrow of data
doesn't overlap and there's no conflict.
What you really want is that F
implement Foo<'x>
for some lifetime 'x
that is shorter than 'a
, and most importantly, that lifetime must be a region within the function, not an enclosing one like 'a
is.
Rust's current solution to this problem is higher-ranked trait bounds. It looks like this:
fn test<'a, F>(data: &'a mut u32)
where F: for<'x> Foo<'x>
{
{
let mut foo: F = Foo::new(data);
foo.apply();
}
println!("{:?}", data);
}
In words, it means the type F
must implement Foo<'x>
for every possible 'x
.
While this version of test
compiles on its own, we cannot actually supply a type that fulfills this constraint, because for every possible lifetime 'a
, there is a distinct type FooS<'a>
that only implements Foo<'a>
. If FooS
had no lifetime parameter and the impl of Foo
for FooS
looked like this:
impl<'a> Foo<'a> for FooS {
then it would be fine, since there is a single type FooS
that implements Foo<'a>
for every possible lifetime 'a
.
Of course, you can't remove the lifetime parameter on FooS
, as it contains a borrowed pointer. The proper solution for this problem is a feature that Rust doesn't have yet: the ability to pass a type constructor (rather than a fully constructed type) as a generic parameter to a function. With this capability, we could call test
with FooS
, a type constructor that needs a lifetime parameter to produce a concrete type, without specifying the concrete lifetime at the call site, and the caller would be able to supply its own lifetime.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With