Skip to content

Commit 071ae80

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

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
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
165+
end the life of `self`. Even if this leads to more verbosity
166+
167+
### `discard` recursively leaks properties without cleaning them up
168+
169+
In Rust, `mem::forget` completely forgets a value, bypassing not only the
170+
value's own `drop()` implementation (the equivalent of our `deinit`) but
171+
also the cleanup of any of its component fields. We could in theory
172+
define `discard` the same way, having it discard the value without even
173+
cleaning up its remaining properties. However, this would make it very
174+
easy to accidentally leak memory.
175+
176+
Recursively leaking fields also creates a backdoor for breaking
177+
the ordering of `deinit` cleanups with lifetime dependencies, which the
178+
original `discard self` design. Despite the prohibition on using
179+
`discard self` outside of a type's original definition, someone could
180+
wrap the type in their own type and use `discard self` to leak the value:
181+
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)