Very often, I lament that the Drop::drop function takes &mut self and not self which would be quite handy for eg. writing a mempool datastructure and plenty of other things.
But I can quite easily write the following and it seems totally safe to me:
Generally the pattern of take(self); forget(self); is at risk of being subtly unsound. Prefer let this = ManuallyDrop::new(this); take(&mut *this); instead, which doesn't use/move self after you've invalidated it.
I this usage is (intended to be[1]) sound, but forget-first is generally preferable anyway.
(I am a member of T-opsem but speaking solely for myself, not for the team.)
I had a crate drop-take that offered something like this, but I yanked it for I don't recall why. The pattern generally works, but is at some risk of drop cycles if you aren't careful.
In general, what do you want a concrete DropByValue impl to look like? If self is passed by value, then Drop will be called again at the end of the function. Or do you expect every single impl to correctly forget(self)? Because that's a huge footgun. The fact that Drop::drop() takes a reference is no accident.
@H2CO3 I gave an example usage. See the Foo/FooInner datatypes. No mem::forget required. drop_by_value is called once on FooInner whenever Foo is dropped.
The only way I could imagine getting a Drop-cycle is if FooInner::drop_by_value constructed a new Foo and dropped it, and of course this is just generally true of having drop construct a new instance of the thing it's meant to be dropping. I suppose it's rendered a little bit easier since you're guaranteed to have everything you need to construct a new Foo, but I still find it hard to believe it's very easy to do by accident.
It's not a particularly large hazard the way you've set it up with a struct DroppedByValue wrapper, but it's more of a pitfall if you give dropping Foo access to Foo by value, instead of splitting it into two parts like you have here.
Why does the builtin Drop::drop take &mut self? well, for one thing, drop_in_place needs to exist in order for a Vec<ComplexDrop> of size 100M to work properly without blowing out resource limits.
I think I was more asking about why, given how useful this pattern seems to be, I couldn't find this alternative rather than why the default is the way it is.
This is really interesting and I am keen to see an answer. When you implement Drop for a type T it comes with some restrictions as far as I understand:
T can't also be Copy.
The drop checker requires that any generic arguments outlive T.
You can't partially move out of T.
Are there any similar restrictions you'd need for DropByValue to be sound?
The restrictions directly transfer if the type DroppedByValue<T> is used, since DroppedByValue has an ordinary Drop implementation. (Well… and then, partial moves are additionally also prevented by the fact that DroppedByValue can only offer access to its contents via Deref[Mut].)
To be precise, there is no requirement that T itself is Copy or drop checker restrictions on T itself, so a type T: Copy can implement DropByValue just fine; but DroppedByValue<T> still won’t become Copy, and the drop checker will care about dead lifetimes in DroppedByValue<T>.