Rust does not allow destructuring types which implement the Drop trait. This means that moving data out of such types is hard and error prone. The rationale is that once fields are moved out, the type's Drop implementation cannot run, which can be undesired. This RFC proposes to allow destructuring anyway, in certain situations.
Motivation
There are real use-cases in which you do want to move out of types that implement Drop. Two main types of patterns that often need it are:
- implementing methods like
into_raw_partswhich explicitly hand over their internals and move the burden of dropping those - "guard" types which might need to be "disarmed"
In both cases, you want to avoid the regular Drop implementation from running at all and get ownership of the fields.
Right now, code that wants to do this needs to use unsafe. For example, std::io::BufWriter::into_parts() has to perform the following gymastics using ManuallyDrop:
When writing this RFC, we even spent some time to make sure this example from std was even correct. By exposing drop_type_destructuring, we can reduce the complexity of such use cases.
Through this, we can avoid Dropping types for which running Drop is very important. However, this is already the case because you could use mem::forget.
Guide-level explanation
drop_type_destructuring adds a new built-in macro, destructure. You can use this macro to destructure types that implement Drop. For example, assuming Foo is a type that implements Drop and has fields a and b, you could write the following:
This moves the field a out of x, and immediately drops b.
Instead of creating a new binding, you can also assign to an existing one by leaving out the let, similarly to destructuring assignment:
When a type implements Drop, this can be because the order in which fields are dropped is very important. It could be unsound to do it in the wrong order. When you write a destructuring, soundly dropping the fields is now your responsibility.
This can be a heavy burden, so if you are the author of a module or crate, you might want to limit other people destructuring your type. The rule for this is that you can only use destructure!() on types in a location where you could also construct that type. This means that in that location, all fields must be visible.
Importantly, this means that this code is not valid:
By using .., we could ignore the fact that b was not visible and move out a anyway. This is undesirable, as it would implicitly run drop(b) even though b was not accessible here. In fact, if there was a requirement that a was dropped before b, it would never be sound to destructure a type Bar in a location where b is not accessible.
Finally, you might ask why we need a macro for this at all. This is because for some types, the behavior is slightly different. Let's use the following example. In Rust right now, this does not compile:
This is because we move the field a out, and Foo implements Drop.
The following does compile:
Similarly, the following also works because we don't have to move out any fields, we can just copy a, because u64 implements Copy.
In the two examples above, Drop still runs for x.
destructure!() represents a different operation. When we use it, we actually move the fields out of the type, and prevent Drop from running.
Preventing Drop from running is not inherently problematic in Rust. You can already do this for any type through mem::forget (and other means), which is safe.
Finally, the following is an example of how one could implement the example from Motivation:
Now with destructure!:
We don't even need unsafe anymore :tada:
Reference-level explanation
A built-in macro with the following "signature" is added to the standard library:
The result of the expansion checks the following requirements and produces a compilation error if any of them are not met:
$patmust be an irrefutable pattern$emust be an owned expression that can be matched by pattern$pat$e's type must be an ADT (structorenum)- The type of
$emust be constructable in the current context, i.e.- All fields must be visible to the current module wrt privacy
- If the type is marked with
#[non_exhaustive], it must be defined in the current crate
The semantic of this macro is equivalent to the following:
- Wrapping
$ein aManuallyDrop - Copying all fields of
$e(for enums all fields of the active variant) - If the
lettoken is present in the macro invocation, fields that are listed in$patare binded to their respective bindings or patterns. Otherwise, they are assigned similarly to destructuring assignment - If
..is present in$pat, all fields that are not mentioned in it are dropped in the order of definition (simialrly to how normallet/destructuring assignment works)
Drawbacks
When implementing this change as a macro expanding to a statement, we do not think there are many drawbacks. However, in the coming section with alternatives we discuss another possible expansion to a pattern which may have a few advantages, but also has many drawbacks which we discuss there.
Rationale and alternatives
Do not support destructuring assignment
Instead of allowing optional let token, we could always ommit it, and consider destructure! to always introduce new bindings.
It's not evident that destructure assignment is particularly useful in situations where destructure! would be used.
Expand to a pattern
Instead of making this macro expand to a statement, containing a pattern and a right hand side, one could imagine a macro that expands just to a pattern:
let destructure! = x;
On its own this could be considered, it also means that whether to use let or not (the previous alternative) isn't a concern for the macro. However, by doing this, we might imply that you can also use destructure! in other places you might see a pattern:
let destructure! = x else ;
match x
Now, it makes sense that patterns in destructure! can be refutable. Even this can be okay, as long as only the top-level pattern is refutable. If we allow nested patterns to be refutable as well, we can construct the following scenario:
match y
Here, if we were to move fields out of Bar from left to right, we might move a, then check whether b matches 1, and if it doesn't we already moved a out.
In theory this is not impossible to implement. By making this expand to code that uses ManuallyDrop, and when a pattern is rejected semantically move values back. However, one can construct even more complicated scenarios than just this one, and actually defining the semantics of this operations quickly becomes incredibly complicated.
Just as a small example of that, consider this:
match z
Where both Baz and Bar implement Drop.
As a final note on this, making destructure!() expand to a pattern on its own is possible, and even refutability is possible if we only allow "one level" of refutability (Some(_), but not Some(1)). However, we simply do not think there are many good uses for this.
The one scenario we could imagine is that might be useful for destructure!() on enums, but we also think that even using destructure!() on enums will be very uncommon (as types implementing Drop are almost always structs) and might not outweigh the increased implementation complexity and the fact that we need to communicate this one level matching rule to users.
Macro expression and Magic type
Instead of having a macro be a statement or pattern, we could make it an expression returning a type (say Destructure<T>) with magic similar to that of a Box and ManuallyDrop. Specifically, it would:
- Allow moving
T's fields out ofDestructure<T>even ifT: Drop - Not drop
TwhenDestructure<T>is dropped - But drop all fields of
TwhenDestructure<T>is dropped (except the moved-out ones) - Be able to be matched directly with a pattern that would match
T
Note that having a macro produce Destructure<T>, as opposed to a function is still required, since we still want to check that T is constructable in the current context.
This would allow writing the BufWriter::into_parts example from Motivation like this:
Or closer to the original code:
The upside of this is that Destructure type can be used as input into functions which are expected to not drop T, such as potential DropOwned:
;
// This is explicitly *not* proposed as part of this RFC,
// but is a kind of pattern that could be allowed by `Destructure`.
The downside of this approach is that it is significantly harder to specify, implement, and teach.
Prior art
@WaffleLapkin wrote a crude polyfill for this using a macro and ManuallyDrop:
/// Destructures `$e` using a provided pattern.
///
/// Importantly, this works with types which implement `Drop` (ofc, this doesn't run the destructor).
;
);
=> ;
=> ;
}
Example usage:
// `destructure` does not run the destructor, so this **doesn't** unlock the lock.
destructure!;
This polyfill has multiple downsides, such as:
- It does not support ommiting fields with
..(this is not possible to support in the polyfill, as you are unable to prove that an expression is owned, without specifying its full type) - It doesn't work with enums with multiple variants, even if you could specify an irrefutable pattern (this theoretically can be supported, but is quite complicated)
- It does not work with tuple struct syntax (impossible to support arbitrary number of fields, hard to support a set number)
Unresolved questions
These mainly relate to the above-mentioned alternatives:
- Do we support
letinside? - Should
destructure!()expand to a statement, or a pattern?
Future possibilities
If we were to choose for the version that expands to a pattern, we could try to define the semantics of refutable patterns with more than one level, assuming this is even possible in a satisfactory and importantly sound manner.
In general, we could add some lints in relation to this feature.
General usage of destructure! is often okay, especially in places where there is visibility of all fields, but it could be used in unintentional ways so a lint in clippy::pedantic for using destructure might be considered.
A lint for using destructure! on types that don't implement Drop at all makes sense regardless. The suggestion there can be to remove destructure! completely as it is not necessary.