Making Drop more magic to make it less magic

This code DOES panic. Try it in playground.

If the match only do partial move like in the following though,

enum MyEnum {
    V1(String),
    V2
}

impl Drop for MyEnum { fn drop(&mut self) { panic!(); } }

fn main() {
  let foo = MyEnum::V1("v1".into());
  if let MyEnum::V1(s) = foo { println!("{}", s) }
}

You get

error[E0509]: cannot move out of type `MyEnum`, which implements the `Drop` trait
  --> src/main.rs:10:10
   |
10 |   if let MyEnum::V1(s) = foo { println!("{}", s) }
   |          ^^^^^^^^^^^-^
   |          |          |
   |          |          hint: to prevent move, use `ref s` or `ref mut s`
   |          cannot move out of here

Deeper thoughts

I believe dropping an object includes two related but different things:

  1. Fulfill the invariant for an object to be dropped (for example, call some FFI to release external resources). After that, the drop clue for the main object is considered being used and so should not be run again
  2. Destruct the object into its parts, and allows further dropping clue on child objects to run

I believe today's Drop is good for step 1, as it expect a fully functional object, including its drop clue. However, trying to do partial move in such a drop method is trying to mix it up with step 2, which is the source of the problems we have today.

Having it in consideration, we can go forward to the right thing without breaking any backward compatibility issues. Here is the proposal:

New trait: Destruct

trait Destruct {
    #[destruct]
    fn destruct(self);    
}
impl<T:Destruct> for Option<T> {
    #[destruct]
    fn destruct(self) {
        match self {
            Some(v) => v.destruct(),
            None => ()
        }
    }
}

Rules:

  • A function/method have #[destruct] attribute must accept a single argument and its first HIR operation must be a pattern match that moves things from the arguments out (we call this "moving match"). This guarantees no references to the destructing object can occur in user code.
  • destruct will be called when the object was implicitly dropped. But if it is destructed through moving matches it is not called.
  • If an object implements both Drop and Destruct, it does not cause E0509: a moving match will call drop before the match, to justify the call. However, partial move in drop will be unconditionally UB.
  • If an object implements only Destruct, it can also be Copy. I don't think this is useful, but there is no reason to stop this.
  • For objects implements only Drop, it behaves exactly the same as today: partial moving match results in E0509. drop can do partial move without UB. We may later consider lint against it and finally obsolete this style.
  • #[destruct] can apply to any functions, as long as it follows the same rule. Especially, this allows closures being declared like this, making it possible to pass a "pattern match snippet" across. However, for methods that call-by-reference rather than call-by-move, the match need not be a moving match.

Compare to the DefaultDrop<T> proposal:

I understand @stepancheg 's DefaultDrop<T> means a T with the drop clue removed. However, it have more magic here, and didn't resolve E0509 at all.

3 Likes