A question about drop check

I was checking the drop check chapter of the nomicon where it says that even though the drop order of items in a tuple is defined, the compiler can't know whether the right Vec in a (Vec<U>, Vec<V>) has a longer lifetime than the left one because Vec<T> implements Drop, the details of which the compiler can't examine.

At first, I assume that this means v1 below still does not strictly outlives v2:

let v1 = Vec::new();
let v2 = Vec::new();

Since even though the drop order is defined, the compiler still can't understand Drops of Vecs.
But this code compiles:

struct Inspector<'a, T>(&'a IDropDifferently<T>);

impl<'a, T> Drop for Inspector<'a, T> {
    fn drop(&mut self) {}
}

struct IDropDifferently<T>(T);

impl<T> Drop for IDropDifferently<T> {
    fn drop(&mut self) {}
}

fn main() {
    let before_inspector = IDropDifferently(55);
    let inspector = Inspector(&before_inspector);
}

So in this case before_inspector does have a longer lifetime than inspector! How are the two cases different? In both cases the drop order is defined. Does (Vec<U>, Vec<V>) automatically have a Drop::drop implementation that the compiler still can't examine and understand so the compiler can't assume that drop order?

Note that the nomicon’s example also features a type, struct World<'a>, that does not have a Drop implementation. The Drop implementation that is added is only for the field’s type, struct Inspector<'a>.

The type World<'a> serves the same role as a tuple would. A simple struct containing two fields with no additional Drop logic. This is the context for the claim made:

Let's do this:

let tuple = (vec![], vec![]);

The left vector is dropped first. But does it mean the right one strictly outlives it in the eyes of the borrow checker? The answer to this question is no. The borrow checker could track fields of tuples separately, but it would still be unable to decide what outlives what in case of vector elements, which are dropped manually via pure-library code the borrow checker doesn't understand.


Also this claim is in contrast with having two separate individual variables. To adapt your example to something that can be translated into code with a tuple, see this which still works:

struct Inspector<'a, T>(&'a IDropDifferently<T>);

impl<'a, T> Drop for Inspector<'a, T> {
    fn drop(&mut self) {}
}

struct IDropDifferently<T>(T);

impl<T> Drop for IDropDifferently<T> {
    fn drop(&mut self) {}
}

fn main() {
    let before_inspector = IDropDifferently(55);
    let mut inspector = None;

    inspector = Some(Inspector(&before_inspector));
}

but once we bundle up before_inspector and inspector together, it fails:

struct Inspector<'a, T>(&'a IDropDifferently<T>);

impl<'a, T> Drop for Inspector<'a, T> {
    fn drop(&mut self) {}
}

struct IDropDifferently<T>(T);

impl<T> Drop for IDropDifferently<T> {
    fn drop(&mut self) {}
}

fn main() {
    let mut before_inspector_and_inspector = (IDropDifferently(55), None);

    before_inspector_and_inspector.1 = Some(Inspector(&before_inspector_and_inspector.0));
}
error[E0597]: `before_inspector_and_inspector.0` does not live long enough
  --> src/main.rs:16:55
   |
14 |     let mut before_inspector_and_inspector = (IDropDifferently(55), None);
   |         ---------------------------------- binding `before_inspector_and_inspector` declared here
15 |
16 |     before_inspector_and_inspector.1 = Some(Inspector(&before_inspector_and_inspector.0));
   |                                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ borrowed value does not live long enough
17 | }
   | -
   | |
   | `before_inspector_and_inspector.0` dropped here while still borrowed
   | borrow might be used here, when `before_inspector_and_inspector` is dropped and runs the destructor for type `(IDropDifferently<i32>, Option<Inspector<'_, i32>>)`

but only as long as Inspector has a Drop implementation, as the following once again works:

struct Inspector<'a, T>(&'a IDropDifferently<T>);

struct IDropDifferently<T>(T);

impl<T> Drop for IDropDifferently<T> {
    fn drop(&mut self) {}
}

fn main() {
    let mut before_inspector_and_inspector = (IDropDifferently(55), None);

    before_inspector_and_inspector.1 = Some(Inspector(&before_inspector_and_inspector.0));
}

So to answer the questions in particular:

I’m not 100% clear what cases you are comparing, but I was trying to make sure the right differences are clear with the above examples. In cases of tuple or the World struct, the drop order is defined but not considered by drop check, as the nomicon explains. It doesn’t explain the rationale behind this approach though if you’d ask me the explanation is simply that it’s just very hard to come up with general rules that support it, and the nomicon does outline the cases that must not compile to show how subtle such rules would have to be. It also calls out that e.g. within a Vec the drop order of the vector elements cannot possibly be known by the compiler as that’s user code doing the dropping, so there’s little gain in special rules for structs. (Other things to consider would include abstraction boundaries in case fields are private, etc…)

TL;DR: The drop order is well-defined for structs, but drop checker does not consider the drop oder.

At no point was the claim that the drop order of fields of a struct like World<'a> is unknown to the compiler because of a Drop::drop implementation. So the answer is no, they don’t have an automatic Drop::drop implementation, though dropping World<'a> or (Vec<U>, Vec<V>) does involve calling Drop::drop implementations of the fields that implement Drop; and the field’s fields.

The argument about Drop::drop – in the context of World<'a>

struct World<'a> {
    inspector: Option<Inspector<'a>>,
    days: Box<u8>,
}

is merely that if Inspector has a Drop::drop implementation, then there are some field orders (inspector before days) which no longer can soundly support inspector borrowing from days, so the compiler / drop checker conservatively disallows the case outright, regardless of the actual field order.

2 Likes

Or a #[may_dangle] on 'a to swear to dropck that Inspector will never access the inner reference in its destructor will work on nightly, too:

#![feature(dropck_eyepatch)]

struct Inspector<'a, T>(&'a IDropDifferently<T>);

unsafe impl<#[may_dangle] 'a, T> Drop for Inspector<'a, T> {
    fn drop(&mut self) {}
}

struct IDropDifferently<T>(T);

impl<T> Drop for IDropDifferently<T> {
    fn drop(&mut self) {}
}

fn main() {
    let mut before_inspector_and_inspector = (IDropDifferently(55), None);

    before_inspector_and_inspector.1 = Some(Inspector(&before_inspector_and_inspector.0));
}

Playground.

With this answer I really only wanted to reference the dropcheck eyepatch RFC, which IMO is great further reading material. This section of the maybe_dangling::MaybeDangling docs might be helpful, too.

1 Like

Thanks for the clarification!

The left vector is dropped first. But does it mean the right one strictly outlives it in the eyes of the borrow checker? The answer to this question is no.

I originally thought the no is explained in this part:

but it would still be unable to decide what outlives what in case of vector elements, which are dropped manually via pure-library code the borrow checker doesn't understand.

So if I understand you correctly, this actually motivates the no: the compiler doesn't want to track everything, for example Vec elements, and fields of structs/tuples, even though their drop orders are defined!

Drop check, and the eye-patch, are highly complex topics anyways. I also remember not really understanding the thing all that well from the first look at the nomicon page. For me it’s experimenting with what the compiler does or does not accept, as well as looking at existing usages of may_dangle in the standard library in detail, that helped form a better overall understanding.

To fully convey the level of complexity of the topic: Eventually I had even found a bug in std where the compiler behavior I’d experimentally grasped and the usage in the library didn’t quite match sensibly, and was indeed unsound. (Caused by some subtle compiler changes, IIRC, that made the struct unsound even though it was sound at the time it was first written.)

5 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.