Skip to content

Commit 67238db

Browse files
committed
New proposal: discard self for types with non-BitwiseCopyable members
1 parent ed81ba4 commit 67238db

File tree

1 file changed

+207
-0
lines changed

1 file changed

+207
-0
lines changed
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# `discard self` for types with non-`BitwiseCopyable` members
2+
3+
* Proposal: [SE-AOEU](aoeu-discard-nontrivial-self.md)
4+
* Authors: [Joe Groff](https://github.com/jckarter)
5+
* Review Manager: TBD
6+
* Status: **Awaiting implementation**
7+
* Implementation: [TBD](TBD)
8+
* Review: ([pitch (TBD)](TBD))
9+
10+
## Introduction
11+
12+
This proposal extends the `discard self` special form to allow for its
13+
use in all non-`Copyable` types with user-defined `deinit`s.
14+
15+
## Motivation
16+
17+
When [SE-0390] introduced non-`Copyable` types, we gave
18+
these types the ability to declare a `deinit` that runs at the end of
19+
their lifetime, as well as the ability for `consuming` methods to
20+
use `discard self` to clean up a value without invoking
21+
the standard `deinit` logic:
22+
23+
```swift
24+
struct Phial {
25+
var phialDescriptor: Int = 0
26+
27+
deinit {
28+
print("automatically closed") }
29+
}
30+
31+
consuming func close() {
32+
print("manually closed")
33+
34+
discard self
35+
}
36+
}
37+
38+
do {
39+
let p1 = Phial()
40+
// only prints "automatically closed"
41+
}
42+
43+
do {
44+
let p2 = Phial()
45+
p2.close()
46+
// only prints "manually closed"
47+
}
48+
```
49+
50+
At the time, we restricted `discard self` to only support types whose
51+
members are all `BitwiseCopyable` in order to limit the scope of the
52+
initial proposal. However, `discard self` is also useful for types
53+
with non-`BitwiseCopyable` members.
54+
55+
## Proposed solution
56+
57+
We propose allowing `discard self` to be used inside of `consuming`
58+
methods which are defined inside of the original type declaration of
59+
any non-`Copyable` type with a `deinit`. `discard self` immediately
60+
ends the lifetime of `self`, destroying any components of `self` that
61+
have not yet been consumed at the point of `discard self`'s execution.
62+
63+
## Detailed design
64+
65+
### Remaining restrictions on `discard self`
66+
67+
This proposal preserves the other restrictions on the use of `discard self`:
68+
69+
- `discard self` can only appear in methods on a non-`Copyable` type that has
70+
a `deinit`.
71+
- `discard self` can only be used in `consuming` methods declared in that type's
72+
original definition (not in any extensions).
73+
- If `discard self` is used on any code path within a method, then every code
74+
path must either use `discard self` or explicitly destroy the value normally
75+
using `_ = consume self`.
76+
77+
As noted in SE-0390, these restrictions ensure that `discard self` cannot be
78+
used to violate an API author's control over a type's cleanup behavior, and
79+
reduce the likelihood of oversights where implicit exits out of a method
80+
unintentionally implicitly invoke the default `deinit` again, so we think
81+
they should remain in place.
82+
83+
### Partial consumption and `discard self`
84+
85+
A type with a `deinit` cannot typically be left in a partially consumed state.
86+
However, partial consumption of `self` is allowed when the remainder of `self`
87+
is `discard`-ed. At the point `discard self` executes, any remaining parts of
88+
`self` which have not yet been consumed immediately get destroyed. This allows
89+
for a `consuming` method to process and transfer ownership of some components
90+
of `self` while leaving the rest to be cleaned up normally.
91+
92+
```swift
93+
struct NC: ~Copyable {}
94+
95+
struct FooBar: ~Copyable {
96+
var foo: NC, bar: NC
97+
98+
deinit { ... }
99+
100+
consuming func takeFooOrBar(_ which: Bool) -> NC {
101+
var taken: NC
102+
if which {
103+
taken = self.foo
104+
discard self // destroys self.bar
105+
} else {
106+
taken = self.bar
107+
discard self // destroys self.foo
108+
}
109+
110+
return taken
111+
}
112+
}
113+
```
114+
115+
## Source compatibility
116+
117+
This proposal generalizes `discard self` without changing its behavior in
118+
places where it was already allowed, so should be fully compatible with
119+
existing code.
120+
121+
## ABI compatibility
122+
123+
This proposal has no effect on ABI.
124+
125+
## Implications on adoption
126+
127+
This proposal should make it easier to use noncopyable types as a resource
128+
management tool by composing them from existing noncopyable types, since
129+
`consuming` methods on the aggregate will now be able to release ownership
130+
of the noncopyable components in a controlled way.
131+
132+
## Alternatives considered
133+
134+
### `discard` only disables `deinit` but doesn't immediately
135+
136+
An alternative design of `discard` is possible, in which `discard self`
137+
by itself only disables the implicit `deinit`, but otherwise leaves the
138+
properties of `self` intact, allowing them to be used or consumed
139+
individually after the `deinit` has been disabled. This could allow for
140+
more compact code in branch-heavy cleanup code, where different branches
141+
want to use different parts of the value. For example, the `FooBar.takeFooOrBar`
142+
example from above could be written more compactly under this model:
143+
144+
```swift
145+
struct FooBar: ~Copyable {
146+
var foo: NC, bar: NC
147+
148+
deinit { ... }
149+
150+
consuming func takeFooOrBar(_ which: Bool) -> NC {
151+
discard self // disable self's deinit
152+
153+
// Since self now has no deinit, but its fields are still initialized,
154+
// we can ad-hoc return them below:
155+
if which {
156+
return self.foo
157+
} else {
158+
return self.bar
159+
}
160+
}
161+
}
162+
```
163+
164+
However, we believe that it is more intuitive for discard to immediately end the life of self, even if this leads to more verbosity. Being able to place discard self at the beginning of a method and forgetting about it could also make code harder to read and make it easy to forget that the default deinit has been disabled when tracing through it.
165+
166+
### `discard` recursively leaks properties without cleaning them up
167+
168+
In Rust, `mem::forget` completely forgets a value, bypassing not only the
169+
value's own `drop()` implementation (the equivalent of our `deinit`) but
170+
also the cleanup of any of its component fields. We could in theory
171+
define `discard` the same way, having it discard the value without even
172+
cleaning up its remaining properties. However, this would make it very
173+
easy to accidentally leak memory.
174+
175+
Recursively leaking fields also creates a backdoor for breaking
176+
the ordering of `deinit` cleanups with lifetime dependencies, which the
177+
original `discard self` design. Despite the prohibition on using
178+
`discard self` outside of a type's original definition, someone could
179+
wrap the type in their own type and use `discard self` to leak the value:
180+
181+
```swift
182+
struct Foo: ~Copyable {
183+
deinit { ... }
184+
}
185+
186+
struct Bar: ~Copyable {
187+
var foo: Foo
188+
189+
consuming func leakFoo() {
190+
// If this discarded `self.foo` without running its deinit,
191+
// it would be as if we'd externally added a method to
192+
// `Foo` that discards self
193+
discard self
194+
}
195+
}
196+
```
197+
198+
Particularly with `~Escapable` types, many safe interfaces rely on having
199+
a guarantee that the `deinit` for a lifetime-dependent type ends inside of
200+
the access in order to maintain their integrity. Having this capability
201+
could be necessary in some situations, but it should not be the default
202+
behavior, and if we add it in the future, it should be an unsafe operation.
203+
204+
## Acknowledgments
205+
206+
Kavon Favardin wrote the initial implementation of `discard self`, and helped
207+
inform the design direction taken by this proposal.

0 commit comments

Comments
 (0)