I am trying to understand this, well, I think I understand it, but it seems questionable. The following (playground) does not compile, with message:
error[E0499]: cannot borrow `x` as mutable more than once at a time
--> src/main.rs:31:13
|
30 | let _b = x.bor();
| - first mutable borrow occurs here
31 | let c = x.bor();
| ^ second mutable borrow occurs here
32 | println!("{}", c.a);
33 | }
| - first borrow might be used here, when `_b` is dropped and runs the `Drop` code for type `B`
For more information about this error, try `rustc --explain E0499`.
struct MyStruct
{
a: usize
}
impl MyStruct
{
fn bor(&mut self) -> B
{
B{ a: &mut self.a }
}
}
struct B<'a>
{
a: &'a mut usize,
}
impl Drop for B<'_>
{
fn drop(&mut self)
{
println!("B dropped!");
}
}
fn main()
{
let mut x = MyStruct{ a: 100 };
let _b = x.bor();
let c = x.bor();
println!("{}", c.a);
}
This can be fixed by either dropping _b earlier by for example putting it in a block
{ let _b = x.bor(); }
or by removing the Drop implementation for struct B.
The problem I see though is that it seems awkward and surprising that adding a Drop implementation to B could cause clients of B to stop compiling. I think it would be better if it should fail to compile even if B doesn't have Drop? Or perhaps at least give a warning that the code is "fragile" in some sense.
I disagree.[1] Reborrowed returns that also Drop basically make the borrow at the call site lexical, if you let it go out of scope. Rust used to have lexically scoped borrows, and you had to do stuff like...
for mut data in input {
{ // Borrow scope
let piter = SomeIterType::skip(&mut self.field1, 1);
let titer = AnotherIter::new(&mut self.field2);
for ((a, v), c) in data.iter_mut().zip(titer).zip(piter) {
// ...
}
} // End borrow scope
for do_more_stuff in &mut self.field1 {
// ...
}
}
...which is just a bunch of noise if the borrowing thing doesn't do anything when dropped. And was also harder and more annoying to learn for newcomers. NLL was a big improvement.
(Reborrowed Droppers aren't quite as bad since you can drop(_) it early without a block, and because sometimes you really care when the drop happens -- e.g. dropping a Mutex lock.)
There are generally a lot of things you can do that will break downstream, and in more severe (e.g. semver violating) ways to boot. E.g. adding fields to your struct that change variance or auto-traits. Imagine having to write...
I can explain the context where I came across this. In my BTreeMap implementation, I tried changing the implementation of Cursor to use references rather than pointers. Everything looked pretty good, but then due (I think) to ArrayVec implementing Drop, one of the standard library tests would no longer compile, due to this type of borrowing. So, on discovering this broke the API, I had to back it out and go back to raw pointers, well, unless I can find some other solution. Perhaps making my own ArrayVec I guess. It certainly makes life difficult!
[ I have not figured out yet why the change had this effect, the raw pointer version still uses ArrayVec. Something to do with lifetimes. Hmm. Is Vec special? Does the compiler know somehow that although it has a Drop implementation, it "doesn't count" ? ]
Edit: ok, I do understand now why the change failed, it is not just whether there is a drop implementation, the lifetimes are also important. The change involved declaring the borrowed reference explicitly, with the raw pointers this didn't happen. Complicated!