(Edit: I realized there's a much shorter way to explain this, and that the detail is unnecessary.)
As @vitalyd says, the code without the Drop
implementation is safe because all the values involved have exactly the same lifetime. Since they all go away simultaneously, no dangling reference can be created.
But here's why implementing Drop
for Foo
breaks the code. The essence of the Drop
trait is that the drop
method gets called when the value becomes unreachable, but while its members are still valid. This means that the owner of a value that implements Drop
must have a strictly shorter lifetime than any references the value contains --- those references had better still be valid when the owner goes away. This ruins the neat story that makes the situation without Drop
safe.
If you are tempted to ask, "What about HashMap
? Doesn't that implement Drop
? Why doesn't that force the lifetimes to be different?" then I acclaim you as a fellow traveler and cringingly refer you to RFC 1327. The upshot: HashMap
, along with many other library types, uses unsafe magic.
(Original post below)
Here's a slightly reduced example that might clarify what's going on:
#![allow(dead_code)]
use std::cell::RefCell;
#[derive(Debug)]
struct Foo<'a>(Option<&'a i32>);
#[derive(Debug)]
struct Reg<'a> {
i: i32,
ri: RefCell<Foo<'a>>
}
fn show_moved<'a>(reg: Reg<'a>) {
println!("{:?}", reg);
}
fn main() {
let r = Reg { i: 42, ri: RefCell::new(Foo(None)) };
*r.ri.borrow_mut() = Foo(Some(&r.i));
println!("{:?}", r);
//show_moved(r);
}
As @vitalyd says, this code is fine because, although one field of r
does end up pointing to another, r
cannot be moved as long as it does so, because it is frozen while borrowed. To see this, uncomment the call to show_moved
: Rust complains that r
cannot be moved because it is borrowed.
Why does adding a Drop
implementation make Rust reject your program?
In my code above, the type of r
is Reg<'x>
, for some new lifetime 'x
that Rust creates afresh for r
's type to use. Rust knows that 'x
must at least cover the span of the program for which r
is alive, since r
contains a &'x i32
value, which must not be allowed to become a dangling pointer.
Similarly, the type of the reference &r.i
is &'y i32
, for some fresh lifetime 'y
, and the type of Foo(Some(&r.i))
is Foo<'y>
. Rust knows that 'y
must be no larger than the span of the program for which r
is alive, since r.i
is a part of r
.
Since the code assigns Foo(Some(&r.i))
to *r.ri.borrow_mut()
, Rust infers that 'x
and 'y
must actually be the same lifetime: the program stores a Foo<'y>
value in a RefCell<Foo<'x>>
.
So for the borrow checker to permit this program, there must be a single lifetime that simultaneously satisfies the requirements Rust has uncovered for both 'x
and 'y
. Looking back, these requirements are: it must cover r
's live span, and it must be no larger than r
's span. There's exactly one lifetime that fits the bill: one that is exactly r
's span.
In this roundabout way, the borrow checker realizes that both reference and referent will go away at the same instant, and thus the code is safe.
This is where Drop
comes in. The idea of the drop
method is that it is called when the value is unreachable, but while the type's own fields are still usable. So if Foo<'x>
above implements Drop
, then the owner of a value of type Foo<'x>
must have a lifetime strictly shorter than 'x
: there must be some interval for the drop
method to run while the &'x i32
reference is still valid.
This "strictly shorter" requirement ruins the neat resolution to the story we told earlier about 'x
and 'y
. Since r
owns a Foo<'x>
, its lifetime must be strictly shorter than 'x
, but since we borrow r.i
, 'x
must cover r
's live span. There is no such lifetime.
A similar issue came up here: