Skip to content

Commit b47f3c8

Browse files
committed
add design notes from lang-team#86
1 parent 5ac487f commit b47f3c8

File tree

2 files changed

+64
-0
lines changed

2 files changed

+64
-0
lines changed

src/SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
- [Design notes](./design_notes.md)
1111
- [Allowing integer literals like `1` to be inferred to floating point](./design_notes/int_literal_as_float.md)
1212
- [Generalizing coroutines](./design_notes/general_coroutines.md)
13+
- [Eager drop](./design_notes/eager_drop.md)

src/design_notes/eager_drop.md

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Eager drop design note
2+
3+
- Project proposal [rust-lang/lang-team#86](https://github.com/rust-lang/lang-team/issues/86)
4+
5+
## Observations
6+
7+
### Any attempt to make drop run more eagerly will have to take borrows into account
8+
9+
The original proposal was to use "CFG dead" as a criteria, but it's pretty clear that this will not work well. Example:
10+
11+
```rust=
12+
{
13+
let x = String::new();
14+
let y = &x;
15+
// last use of x is here
16+
println!("{}", y);
17+
// but we use y here
18+
...
19+
}
20+
```
21+
22+
Here, the fact that `y` (indirectly) uses `x` feels like an important thing to take into account.
23+
24+
### Some destructors can be run "at any time"...
25+
26+
Some destructors have very significant side-effects. The most notable example is dropping a lock guard.
27+
28+
Others correspond solely to "releasing resources": freeing memory is the most common example, but another might be replacing an entry in a table because you are done using it.
29+
30+
### ...but sometimes that significance is only known at the call site
31+
32+
However, it can be hard to know what is significant. For a lock guard, for example, if the lock is just being used to guard the data, then moving the lock release early is actually _desirable_, because you want to release the lock as soon as you are doing changing the data. But sometimes you have a `Mutex<()>`, in which case the lock has extra semantics. It's hard to know for sure.
33+
34+
### Smarter drop placement will mean that adding uses of a variable changes when its destructor runs
35+
36+
This is not necesarily a problem, but it's an obvious implication: right now, the drop always runs when we exit the scope, so adding further uses to a variable has no effect, but that would have to change. That could be surprising (e.g., adding a debug printout changes the time when a lock is released).
37+
38+
In contrast, if you add an early drop `drop(foo)` today, you get helpful error messages when you try to use it again.
39+
40+
In other words, it's useful to have the _destructor_ occurring at a known time (sometimes...).
41+
42+
### Today's drop rules are, however, a source of confusion
43+
44+
The `let _guard = foo` pattern can be easily confused with `let _ = foo`. `let guard = foo; ...; drop(guard);` has the advantage of explicitness, so does something like `foo.with(|guard| ...)`
45+
46+
Temporary rules interact with `match` in ways that surprise people:
47+
48+
```rust=
49+
match foo.lock().data.copy_out() {
50+
...
51+
} // lock released here!
52+
```
53+
54+
Temporary rules interact poorly with unsafe code today, e.g. `CString::new().as_ptr()` is a known footgun. Would this change help? Or perhaps just change the footguns around. (The change as _written_ would not help here, because the temporary would be dropped probably _even earlier_.)
55+
56+
## Alternatives
57+
58+
- Scoped methods
59+
- let blocks
60+
- "Defer" type constructs or scoped guard type constructs from other languages
61+
- Go
62+
- D
63+
- Python

0 commit comments

Comments
 (0)