|
| 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