This the second RFC regarding to the Drop
trait. The first one is here.
Summary
This rfc introduce a new trait Destruct
to move some of the Drop
duty into itself, to allow custom pattern match to move fields out.
The trait is defined as the following:
trait Destruct {
#[match]
fn destruct(self);
}
Which require the #[match]
attribute described in another RFC.
Motivation
Right now, moving a type that is Drop
from a pattern match results in E0509. Also, as the Drop::drop
method receives a borrow, it do not have full control on the object so we have to do some unsafe trick to enable partial move etc.
In theory, dropping an object can be divided into two steps:
- Do everything necessary to announce this object is dropped. This should only be done once, and after this the object is no longer a valid object. This requires only a mutable borrow.
- Depart the object into its pieces, and then drop all child objects. This requires full object ownership.
Today, Rust object that do not implement Drop
can optionally go through step 2 via pattern match, and those implement Drop
expected to go through step 1 via Drop::drop
. However, as Drop::drop
do not have full object ownership, it have to use unsafe tricks to do the step 2 jobs when necessary. This is why this proposal is needed.
Things allowed by this RFC
-
The rule that disallow moving out from a
Drop
type is relaxed: moving is allowed as long as the type implementsDestruct
. -
Programmers can now write safe code to write complicated dropping logic (child object dropping order can be precisely specified).
Why new trait
This RFC intended to be 100% backward compatible. This means the existing code does not need to know about the meaning of this RFC; they simply works the same way it did.
For this reason, if we simply add new methods to the Drop
type we have to specify how and when the new methods to be called, and a lot of compatibility issues have to be addressed.
Introducing a new trait, on the other hand, is easier as existing code need not know about it.
Demostration of benifit
The following code was stolen from std::mem::ManualDrop
documentation (with minimal modification that require a call before the drop logic):
use std::mem::ManuallyDrop;
struct Peach;
struct Banana;
struct Melon;
struct FruitBox {
// Immediately clear there’s something non-trivial going on with these fields.
peach: ManuallyDrop<Peach>,
melon: Melon, // Field that’s independent of the other two.
banana: ManuallyDrop<Banana>,
}
impl Drop for FruitBox {
fn drop(&mut self) {
FruitBox::notify_owner("consumed!"); // Always called before destruct
unsafe {
// Explicit ordering in which field destructors are run specified in the intuitive
// location – the destructor of the structure containing the fields.
// Moreover, one can now reorder fields within the struct however much they want.
ManuallyDrop::drop(&mut self.peach);
ManuallyDrop::drop(&mut self.banana);
}
// After destructor for `FruitBox` runs (this function), the destructor for Melon gets
// invoked in the usual manner, as it is not wrapped in `ManuallyDrop`.
}
}
This can be rewitten without ManuallyDrop
and unsafe code as the following:
struct Peach;
struct Banana;
struct Melon;
struct FruitBox {
box_id: usize,
peach: Peach,
melon: Melon, // Field that’s independent of the other two.
banana: Banana,
}
impl Drop for FruitBox {
fn drop(&mut self) {
notify_owner!("{} consumed!", self.box_id); // Always called before destruct
}
}
impl Destruct for FruitBox {
#[match]
fn destruct(self) {
match self {
//Note: notify can be called here as well; but it is not recommended
// because destruct can be bypassed.
FruitBox{box_id:_, peach, melon, banana} => {
self.peach.destruct();
self.banana.destruct();
// calling melon.destruct() is optional
}
}
}
}
Guide-level explaination
Definition
trait Destruct {
#[match]
fn destruct(self);
}
The attribute #[match]
attribute indicates this method must in its very first step, pattern match on self
that moves, and so self
will be invalid in any other occurrences, as self
cannot be rebinded in Rust. See this RFC for detailed restrictions.
Derive
Destruct
can be derived to all user types with the usual #[derive]
attribute:
#[derive(Destruct,Clone)]
struct MyStruct(...)
The default code is just a simple ignoring match, and after optimization, nothing (of cause, as usual, the drop glue on child objects need to be called). This is not very useful for types that is not Drop
, but if it is Drop
, this changes the expectation of the drop
method: partial move is now not allowed.
Restriction
If a type implements Destruct
, its Drop
cannot do partial move. Attempts to do so results in undefined behavior.
Reference-level explaination
Invocation
Destruct::destruct
will be called automatically whenever a value implements Destruct
out of scope:
{
let destructable = Destructable::new(...);
// Generated or manually
destructable.destruct();
}
However, an explicit pattern match that moves the value will bypass Destruct::destruct
.
{
let destructable = Destructable::new(...);
match destructable {
...
}
//No code generated
}
Interaction with Drop
If a type implements both Drop
and Destruct
, Drop::drop
will be called before any pattern match that moves the value.
{
let destruct_drop= DestructDrop::new(...);
// generate only: (the users are not able to write this, as they are today)
// destruct_drop.drop()
match destruct_drop { // Note, E0509 is not emitted
...
}
}
{
let destruct_drop= DestructDrop::new(...);
// generate only when destruct is not used manually;
// if calling destruct manually, drop will be called inside destruct,
// before the pattern match
// destruct_drop.drop()
// generated or manually
destruct_drop.destruct();
}
Compatibility
If a type implements both Drop
and Destruct
, the drop
implementation will have to ensure when it returns, the object is still valid except that the drop glue is not to be called again. This means it should not do partial move, even in unsafe code. Attempts to do so will be unconditional UB, because when it eventually being pattern matched, there will be uninitialized values being accessed.
The Drop
implementation writers should also keep in mind that the drop
method usually don't have to take care its child objects. Let the child objects do their things is the best. In Destruct
the programmer can control how the child objects should be handled.
This will not affect existing code: if a type does not implement Destruct
, its behavior do not change.
Drawbacks
Increased complexity by introducing a new trait, also increased learning difficulty as users have to learn the restrictions behind the mechanism.
Rationale and alternatives
Alt 1: Do nothing
We then have same problems we have today: not able to move out from Drop
types without unsafe code. The Drop
method is unintuitive and the theory behind it is not convincing enough.
Alt 2: Define destruct
method without #[match]
, or limit the #[match]
to Destruct
only
We expect the method reader knows that they were constrained to do things in a specific way. An attribute will help them identify this is the case and they can understand by reading the documentation. As #[match]
seems to have no use out of Destruct
, we may initially do this for Destruct
only. Then we need to answer questions about "why I cannot use this attribute in other context".
Alt 3: Alter the Drop
to require destructure of the Drop
type
Many other proposals of this exists. The main problem is backward compatibility, and interaction with pattern matching destructure.
Here is @CAD97's proposal:
trait Drop {
fn drop(&mut self);
fn destroy(#[no_drop_glue] self) {}
}
This can be done in a way to have self
typed ManuallyDrop
or similar. (To be continued)
Alt 4: Introduce a magic container for Drop
types having drop glue removed
This is a proposal from @stepancheg. However in rust, we tent to have more magical traits and less magical containers. For example, Rc
and Arc
used to be magical containers, and now their magic were removed.
It also requires to alter the existent Drop
, and so need to justify compatibility issues.
However, this is also a very attractive alternative and have its beauty. Maybe we can combine the idea to introduce a Destruct
trait that accepts a magical container instead of marking the method.
Prior Art
There are a lot of discussions before...
and so on...
(TODO)
Unresolved questions
The name of the trait and the exact definition. The exact wording of the behavior and restriction.
Updated to address @CAD97 's comment.