Drop interactions with conditional moves


#1

If I have the following code:

struct HasDrop;

impl Drop for HasDrop {
    fn drop(&mut self) {
        println!("Dropping!");
    }
}

fn main() {
    println!("First");
    {
        let a = HasDrop;
        if false {
            let b = a;
        }
        println!("Middle")
    }
    println!("Last");
}

This prints:

First Middle Dropping! Last

And if the condition in the if is true, it prints:

First Dropping! Middle Last

This requires runtime overhead in that additional state must be stored with the object to allow the destruction of the object to happen at the end of the containing block, only if it was not moved as part of the true branch of the if statement.

What is the likelihood that this could be changed so that the drop always happens on the false branch of the if statement so that if the type is not moved, it is dropped instead. This would make things more consistent, in that objects would not be in a valid state while not being accessible (i.e. where the code above prints ‘Middle’).


#2

This was recently discussed extensively. Three proposals were offered:

  • Dynamic Drop: Drop as late as possible (Run time cost, what we have now)
  • Eager Drop: Drop as soon as possible (???)
  • Static Drop: Insert secret “else” blocks and drop in those (No run time cost)

The other proposals were deemed too surprising. Especially if the thing being dropped is e.g. a Mutex. The static drop proposal involved a convoluted system of “quiet drop” and “noisy drop” types to only sometimes warn about conditional drops.

Right now it’s actually worse than you think. Every type that implements drop has a secret byte appended to it to track if it has been dropped. This is being changed to only include a secret boolean in the stack frame, and only for values that are “conditionally moved”. Conditional moves are reasonably rare, so this shouldn’t be a big performance issue.

You can read the RFC and discussion here: https://github.com/rust-lang/rfcs/pull/320


#3

Thanks for the link to the RFC.


#4

I’m still reading the RFC’s but was disallowing conditional moves considered? By this, I mean, if the drop obligations different at a merge point in the control flow graph, the program is ill-formed. That means that instead of writing the above code, I would have to write:

struct HasDrop;

impl Drop for HasDrop {
    fn drop(&mut self) {
        println!("Dropping!");
    }
}

fn main() {
    println!("First");
    {
        let a = HasDrop;
        if false {
            let b = a;
        } else {
            a.drop();
        }
        println!("Middle")
    }
    println!("Last");
}

I’m not sure if a.drop is valid syntax, but there could be a way to drop something early and if that’s accounted for, the surprise aspect of static drop would be removed at the expensive of being explicit about lifetimes.

This seems like a win compared to the dynamic case, since it’s also arguable that having a variable’s lifetime last longer than it can be usefully referenced is confusing.


#5

Yes, it was considered. (That link comes from the older RFC covering this topic. https://github.com/rust-lang/rfcs/pull/210 )