Skip to content

Commit 704a3d2

Browse files
initial text for drop-type-destructuring
Co-authored-by: Waffle Lapkin <[email protected]>
1 parent e17e19a commit 704a3d2

File tree

1 file changed

+368
-0
lines changed

1 file changed

+368
-0
lines changed

text/3738-drop-type-destructuring.md

+368
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
- Feature Name: `drop_type_destructuring`
2+
- Start Date: 2024-12-08
3+
- RFC PR: [rust-lang/rfcs#3738](https://github.com/rust-lang/rfcs/pull/3738)
4+
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)
5+
6+
# Summary
7+
[summary]: #summary
8+
9+
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.
10+
11+
# Motivation
12+
[motivation]: #motivation
13+
14+
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:
15+
16+
- implementing methods like `into_raw_parts` which explicitly hand over their internals and move the burden of dropping those
17+
- "guard" types which might need to be "disarmed"
18+
19+
In both cases, you want to avoid the regular `Drop` implementation from running at all and get ownership of the fields.
20+
21+
Right now, code that wants to do this needs to use `unsafe`. For example, [`std::io::BufWriter::into_parts()`](https://doc.rust-lang.org/stable/std/io/struct.BufWriter.html#method.into_parts) has to perform the following gymastics using `ManuallyDrop`:
22+
23+
```rust
24+
pub fn into_parts(self) -> (W, Result<Vec<u8>, WriterPanicked>) {
25+
let mut this = ManuallyDrop::new(self);
26+
let buf = mem::take(&mut this.buf);
27+
let buf = if !this.panicked { Ok(buf) } else { Err(WriterPanicked { buf }) };
28+
29+
// SAFETY: double-drops are prevented by putting `this` in a ManuallyDrop that is never dropped
30+
let inner = unsafe { ptr::read(&this.inner) };
31+
32+
(inner, buf)
33+
}
34+
```
35+
36+
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.
37+
38+
Through this, we can avoid `Drop`ping types for which running `Drop` is very important. However, this is already the case because you could use `mem::forget`.
39+
40+
# Guide-level explanation
41+
[guide-level-explanation]: #guide-level-explanation
42+
43+
`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:
44+
45+
```rust
46+
fn example(x: Foo) {
47+
destructure!(let Foo { a, b: _ } = x);
48+
// yay we own `a` now, we can even drop it!
49+
drop(a);
50+
}
51+
```
52+
53+
This moves the field `a` out of `x`, and immediately drops `b`.
54+
Instead of creating a new binding, you can also assign to an existing one by leaving out the `let`, similarly to destructuring assignment:
55+
56+
```rust
57+
fn example(x: Foo) {
58+
let mut a = vec![1, 2, 3];
59+
60+
destructure!(Foo { a, b: _ } = x);
61+
// yay we own `a` now, we can even drop it!
62+
drop(a);
63+
}
64+
```
65+
66+
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.
67+
68+
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.
69+
70+
Importantly, this means that this code is not valid:
71+
72+
```rust
73+
mod foo {
74+
pub struct Bar {
75+
pub a: Vec<u8>,
76+
b: Vec<u8>,
77+
}
78+
}
79+
80+
fn example(x: foo::Bar) {
81+
destructure!(let Foo{ a, .. } = x)
82+
}
83+
```
84+
85+
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.
86+
87+
Finally, you might ask why we need a macro for this at all. This is because for some types, the behavior is slightly different.
88+
Let's use the following example. In Rust right now, this does not compile:
89+
90+
```rust
91+
struct Foo {
92+
a: Vec<u64>
93+
}
94+
95+
impl Drop for Foo {
96+
fn drop(&mut self) {}
97+
}
98+
99+
fn example(x: Foo) {
100+
// error: cannot move out of type `Foo`, which implements the `Drop` trait
101+
let Foo { a } = x;
102+
}
103+
```
104+
105+
This is because we move the field `a` out, and `Foo` implements `Drop`.
106+
The following does compile:
107+
108+
```rust
109+
struct Foo {
110+
a: Vec<u64>
111+
}
112+
113+
impl Drop for Foo {
114+
fn drop(&mut self) {}
115+
}
116+
117+
fn example(x: Foo) {
118+
// fine, because we don't actually move any fields out
119+
let Foo { .. } = x;
120+
}
121+
```
122+
123+
Similarly, the following also works because we don't have to move out any fields, we can just copy `a`, because `u64` implements `Copy`.
124+
125+
```rust
126+
struct Foo {
127+
a: u64
128+
}
129+
130+
impl Drop for Foo {
131+
fn drop(&mut self) {}
132+
}
133+
134+
fn example(x: Foo) {
135+
// Totally fine, we just copy a
136+
let Foo { a } = x;
137+
}
138+
```
139+
140+
In the two examples above, `Drop` still runs for `x`.
141+
`destructure!()` represents a different operation. When we use it, we actually move the fields out of the type, and prevent `Drop` from running.
142+
143+
```rust
144+
struct Foo {
145+
a: u64
146+
}
147+
148+
impl Drop for Foo {
149+
fn drop(&mut self) {}
150+
}
151+
152+
fn example(x: Foo) {
153+
// `a` is moved out of `x`, Drop never runs for `x`
154+
destructure!(let Foo { a } = x);
155+
}
156+
```
157+
158+
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.
159+
160+
161+
Finally, the following is an example of how one could implement the example from [Motivation](#motivation):
162+
163+
```rust
164+
pub fn into_parts(self) -> (W, Result<Vec<u8>, WriterPanicked>) {
165+
let mut this = ManuallyDrop::new(self);
166+
let buf = mem::take(&mut this.buf);
167+
let buf = if !this.panicked { Ok(buf) } else { Err(WriterPanicked { buf }) };
168+
169+
// SAFETY: double-drops are prevented by putting `this` in a ManuallyDrop that is never dropped
170+
let inner = unsafe { ptr::read(&this.inner) };
171+
172+
(inner, buf)
173+
}
174+
```
175+
176+
Now with `destructure!`:
177+
178+
```rust
179+
pub fn into_parts(self) -> (W, Result<Vec<u8>, WriterPanicked>) {
180+
destructure!(let Self { buf, inner, panicked } = self);
181+
let buf = if !this.panicked { Ok(buf) } else { Err(WriterPanicked { buf }) };
182+
183+
(inner, buf)
184+
}
185+
```
186+
187+
We don't even need unsafe anymore :tada:
188+
189+
# Reference-level explanation
190+
[reference-level-explanation]: #reference-level-explanation
191+
192+
A built-in macro with the following "signature" is added to the standard library:
193+
194+
```rust
195+
#[macro_export]
196+
macro_rules! destructure {
197+
((let)? $pat:pat = $e:expr) => { /* built-in */ }
198+
}
199+
```
200+
201+
The result of the expansion checks the following requirements and produces a compilation error if any of them are not met:
202+
- `$pat` must be an irrefutable pattern
203+
- `$e` must be an owned expression that can be matched by pattern `$pat`
204+
- `$e`'s type must be an ADT (`struct` or `enum`)
205+
- The type of `$e` must be constructable in the current context, i.e.
206+
- All fields must be visible to the current module wrt privacy
207+
- If the type is marked with `#[non_exhaustive]`, it must be defined in the current crate
208+
209+
The semantic of this macro is equivalent to the following:
210+
1. Wrapping `$e` in a `ManuallyDrop`
211+
2. Copying **all** fields of `$e` (for enums all fields of the active variant)
212+
3. If the `let` token is present in the macro invocation, fields that are listed in `$pat` are binded to their respective bindings or patterns. Otherwise, they are assigned similarly to destructuring assignment
213+
4. If `..` is present in `$pat`, all fields that are not mentioned in it are dropped in the order of definition (simialrly to how [normal `let`/destructuring assignment works](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=3abd3aebd3378690ff3d2006e12d4120))
214+
---
215+
216+
This is the technical portion of the RFC. Explain the design in sufficient detail that:
217+
218+
- Its interaction with other features is clear.
219+
- It is reasonably clear how the feature would be implemented.
220+
- Corner cases are dissected by example.
221+
222+
The section should return to the examples given in the previous section, and explain more fully how the detailed proposal makes those examples work.
223+
224+
# Drawbacks
225+
[drawbacks]: #drawbacks
226+
227+
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.
228+
229+
# Rationale and alternatives
230+
[rationale-and-alternatives]: #rationale-and-alternatives
231+
232+
## Do not support destructuring assignment
233+
234+
Instead of allowing optional `let` token, we could always ommit it, and consider `destructure!` to always introduce new bindings.
235+
236+
It's not evident that destructure assignment is particularly useful in situations where `destructure!` would be used.
237+
238+
## Expand to a pattern
239+
240+
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:
241+
242+
```rust
243+
let destructure!(Foo { a, b }) = x;
244+
```
245+
246+
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:
247+
248+
```rust
249+
let destructure!(Foo::A { a }) = x else {
250+
panic!("not Foo::A")
251+
};
252+
253+
match x {
254+
destructure!(Foo::A { a }) => /* ... */,
255+
Foo::B { .. } => panic!("unexpected Foo::B")
256+
}
257+
```
258+
259+
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:
260+
261+
```rust
262+
match y {
263+
destructure!(Bar { a, b: 1 }) => /* ... */,
264+
_ => panic!(".b != 1")
265+
}
266+
```
267+
268+
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.
269+
270+
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.
271+
272+
Just as a small example of that, consider this:
273+
274+
```rust
275+
match z {
276+
destructure!(Baz { a, b: destructure!(Bar { a, b: 1 }) }) => /* ... */,
277+
_ => panic!(".b.b != 1")
278+
}
279+
```
280+
281+
Where both `Baz` and `Bar` implement `Drop`.
282+
283+
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.
284+
The one scenario we could imagine is that might be useful for `destructure!()` on `enum`s, but we also think that even using `destructure!()` on `enum`s will be very uncommon (as types implementing `Drop` are almost always `struct`s) and might not outweigh the increased implementation complexity and the fact that we need to communicate this one level matching rule to users.
285+
286+
# Prior art
287+
[prior-art]: #prior-art
288+
289+
@WaffleLapkin wrote a crude polyfill for this using a macro and `ManuallyDrop`:
290+
291+
```rust
292+
/// Destructures `$e` using a provided pattern.
293+
///
294+
/// Importantly, this works with types which implement `Drop` (ofc, this doesn't run the destructor).
295+
#[macro_export]
296+
macro_rules! destructure {
297+
($Type:ident { $($f:tt $(: $rename:pat)? ),+ $(,)? } = $e:expr) => (
298+
// FIXME: use $crate:: paths
299+
let tmp = $crate::unstd::_macro_reexport::core::mem::ManuallyDrop::new($e);
300+
301+
// assert that `$e` is an owned expression, rather than `&Type`
302+
if false {
303+
#[allow(unreachable_code)]
304+
let _assert_owned_expr = [&tmp, &$crate::unstd::_macro_reexport::core::mem::ManuallyDrop::new($Type { $($f: todo!()),* })];
305+
};
306+
307+
$(
308+
let $crate::destructure!(@_internal_pat_helper $f $($rename)?)
309+
// safety: `$e` is of type `$Type<..>` (as asserted above),
310+
// so we have ownership over it's fields.
311+
// `$f` is a field of `$Type<..>` (as asserted above).
312+
// `$e` is moved into a `ManuallyDrop`, which means its `drop` won't be run,
313+
// so we can safely move out its
314+
// the pointer is created from a reference and is thus valid
315+
= unsafe { $crate::unstd::_macro_reexport::core::ptr::read(&tmp.$f) };
316+
)+
317+
318+
// remove the temporary we don't need anymore.
319+
// doesn't actually drop, since `ManuallyDrop`.
320+
_ = {tmp};
321+
);
322+
(@_internal_pat_helper $f:tt) => ($f);
323+
(@_internal_pat_helper $f:tt $rename:pat) => ($rename);
324+
}
325+
```
326+
Example usage:
327+
```rust
328+
// `destructure` does not run the destructor, so this **doesn't** unlock the lock.
329+
destructure!(Lock { file, mode: _ } = self);
330+
```
331+
This polyfill has multiple downsides, such as:
332+
- 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)
333+
- 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)
334+
- It does not work with tuple struct syntax (impossible to support arbitrary number of fields, hard to support a set number)
335+
336+
---
337+
338+
Discuss prior art, both the good and the bad, in relation to this proposal.
339+
A few examples of what this can include are:
340+
341+
- For language, library, cargo, tools, and compiler proposals: Does this feature exist in other programming languages and what experience have their community had?
342+
- For community proposals: Is this done by some other community and what were their experiences with it?
343+
- For other teams: What lessons can we learn from what other communities have done here?
344+
- Papers: Are there any published papers or great posts that discuss this? If you have some relevant papers to refer to, this can serve as a more detailed theoretical background.
345+
346+
This section is intended to encourage you as an author to think about the lessons from other languages, provide readers of your RFC with a fuller picture.
347+
If there is no prior art, that is fine - your ideas are interesting to us whether they are brand new or if it is an adaptation from other languages.
348+
349+
Note that while precedent set by other languages is some motivation, it does not on its own motivate an RFC.
350+
Please also take into consideration that rust sometimes intentionally diverges from common language features.
351+
352+
# Unresolved questions
353+
[unresolved-questions]: #unresolved-questions
354+
355+
These mainly relate to the above-mentioned alternatives:
356+
357+
- Do we support `let` inside?
358+
- Should `destructure!()` expand to a statement, or a pattern?
359+
360+
# Future possibilities
361+
[future-possibilities]: #future-possibilities
362+
363+
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.
364+
365+
In general, we could add some lints in relation to this feature.
366+
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.
367+
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.
368+

0 commit comments

Comments
 (0)