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