While learning about Rust's lifetime management, I encountered the below issue.
use std::cell::RefCell;
type TypeA<'a> = Box<dyn TraitA + 'a>;
trait TraitA {}
struct A<'a, 'b> {
xs: &'a RefCell<Vec<TypeA<'b>>>,
}
impl<'a, 'b> TraitA for A<'a, 'b> {}
fn main() {
let v = RefCell::new(Vec::new());
let a = A { xs: &v };
v.borrow_mut().push(Box::new(a));
}
This code fails to compile with the following error:
error[E0597]: `v` does not live long enough
--> src/main.rs:205:21
|
204 | let v = RefCell::new(Vec::new());
| - binding `v` declared here
205 | let a = A { xs: &v };
| ^^ borrowed value does not live long enough
206 | v.borrow_mut().push(Box::new(a));
207 | }
| -
| |
| `v` dropped here while still borrowed
| borrow might be used here, when `v` is dropped and runs the destructor for type `RefCell<Vec<Box<dyn TraitA>>>`
I don't understand why the compiler complains that v is dropped while still borrowed. I think the compiler is trying to avoid the situation where drop implementation for RefCell<Vec<Box<dyn TraitA>>> might somehow touch a reference to v after it has been dropped. I suspect I may have made a mistake with lifetime parameters (especially the second one, 'b), but I don't fully understand the issue.
Meanwhile, the following code does compile without any problems:
use std::cell::RefCell;
struct A<'a> {
xs: &'a RefCell<Vec<Box<A<'a>>>>,
}
fn main() {
let v = RefCell::new(Vec::new());
let a = A { xs: &v };
v.borrow_mut().push(Box::new(a));
}
In this example, I used A directly instead of a trait object, which effectively reduces the number of lifetime parameter for A. How does the lifetime management differ from the previous code, exactly?
The difference between the two cases is that dyn TraitA is not known not to have a destructor. That is, suppose you wrote in the second case:
impl Drop for A<'_> {
fn drop(&mut self) {}
}
Then the code would not compile. This is because the drop() function could access self.xs, which would be invalid because v had been dropped. Because the concrete type behind dyn TraitA may or may not actually implement Drop, dyn Trait is treated as if it always is; essentially, the compiler automatically provides
impl Drop for dyn TraitA + '_ {
fn drop(&mut self) { ...vtable dispatch magic... }
}
But, in general, you will find that it is very hard to do anything useful with a “self-referential” data type. The warning sign of this situation is that you have 'a appearing twice in a type: once as the lifetime of the reference &'a ..., and again inside the mutable (or more precisely, invariant) data type referred to by the reference; the simplest example of such a problematic type is &'a mut MyStruct<'a>, but &'a RefCell<MyStruct<'a>> has the same problem.
You've created something self-referencial by pushing a borrow of v into v. That requires borrowing the structure for the rest of its validity. But you can't invoke a destructor which may observe the borrow, as such a destructor requires exclusive access.
Trait object destructors are always considered to observe their captured borrows.
There's an unsafe, unstable mechanism to tell the compiler that you have a destructor that doesn't observe some of its borrows (generic parameters), and many std data structures (like Vec) make use of that unstable mechanism.
Thank you everyone! All the answers and links provided are really helpful. By the way, I have one more follow-up question, @kpreid.
If I understand correctly, I can illustrate the problem like below:
fn main() {
let v = RefCell::new(Vec::new());
let a = A { xs: &v };
v.borrow_mut().push(Box::new(a));
// drop(v); // This *could* access `self.xs`
}
Apparently, this is a problem as we are holding a mutable reference for v(&mut self of drop) while an immutable reference for v(self.xs in drop) exists.
But I don't understand this sentence. Are the notions of "drop" in "because v had been dropped" and in "the drop() could access self.xs" different? To clarify, does "dropping something" happen before "calling its drop()", and theoretically make all the borrows invalid except for the exclusive one used by its drop()? (I feel like I'm going down a rabbit hole...)
It seems like the compiler also treats these as separate steps, especially since the error message says:
borrow might be used here, when `v` is dropped and runs the destructor for type `RefCell<Vec<Box<dyn TraitA>>>