Skip to content

Commit faa2847

Browse files
committed
expand on limitations of noncopyable types and generics
also includes tips for working around the limitations.
1 parent bbcb22a commit faa2847

File tree

1 file changed

+91
-8
lines changed

1 file changed

+91
-8
lines changed

proposals/NNNN-noncopyable-structs-and-enums.md

Lines changed: 91 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# `@noncopyable` structs and enums
22

33
* Proposal: [SE-NNNN](NNNN-noncopyable-structs-and-enums.md)
4-
* Authors: [Joe Groff](https://github.com/jckarter), [Michael Gottesman](https://github.com/gottesmm), [Andrew Trick](https://github.com/atrick)
4+
* Authors: [Joe Groff](https://github.com/jckarter), [Michael Gottesman](https://github.com/gottesmm), [Andrew Trick](https://github.com/atrick), [Kavon Farvardin](https://github.com/kavon)
55
* Review Manager: TBD
66
* Status: **Partially implemented** as `@_moveOnly` with `-enable-experimental-move-only`
77

@@ -116,7 +116,7 @@ class SharedFile {
116116

117117
### Restrictions on use in generics
118118

119-
Noncopyable types may have generic parameters:
119+
Noncopyable types may have generic type parameters:
120120

121121
```swift
122122
// A type that reads from a file descriptor consisting of binary values of type T
@@ -127,20 +127,103 @@ struct TypedFile<T> {
127127

128128
func read() -> T { ... }
129129
}
130+
131+
let byteFile: TypedFile<UInt8> // OK
130132
```
131133

132-
However, at this time, noncopyable types are not allowed to conform to
133-
protocols, and they cannot be used as type arguments when instantiating
134-
generic types or calling generic functions. A value of noncopyable type also
135-
cannot be stored inside of an `Any` or other existential.
136-
(Lifting these limitations is discussed under Future Directions.)
134+
However, at this time, noncopyable types themselves are not allowed to be used
135+
as a generic type. This means a noncopyable type _cannot_:
136+
137+
- conform to protocols, like `Error` or `Sendable`.
138+
- serve as a type witness for an `associatedtype` requirement.
139+
- be used as a type argument when instantiating generic types or calling generic functions.
140+
- be cast to `Any` or any other existential.
141+
- be accessed through reflection.
142+
- appear in a tuple.
143+
144+
The reasons for these restrictions and ways of lifting them are discussed under
145+
Future Directions.
146+
147+
Since a good portion of Swift's standard library rely on generics, there are a
148+
a number of common types and functions that will not work with today's
149+
noncopyable types:
137150

138151
```swift
139152
// ERROR: Cannot use noncopyable type FileDescriptor in generic type Optional
140153
let x = Optional(FileDescriptor(open("/etc/passwd", O_RDONLY)))
154+
155+
// ERROR: Cannot use noncopyable type FileDescriptor in generic type Array
156+
let fds: [FileDescriptor] = []
157+
158+
// ERROR: Cannot use noncopyable type FileDescriptor in generic type Any
159+
print(FileDescriptor(-1))
160+
161+
// ERROR: Noncopyable struct SocketEvent cannot conform to Error
162+
@noncopyable enum SocketEvent: Error {
163+
case requestedDisconnect(SocketPair)
164+
}
165+
```
166+
167+
For example, the `print` function expects to be able to convert its argument to
168+
`Any`, which is a copyable value. Internally, it also relies on either
169+
reflection or conformance to `CustomStringConvertible`. Since a noncopyable type
170+
can't do any of those, a suggested workaround is to explicitly define a
171+
conversion to `String`:
172+
173+
```swift
174+
extension FileDescriptor /*: CustomStringConvertible */ {
175+
var description: String {
176+
return "file descriptor #\(fd)"
177+
}
178+
}
179+
180+
let fd = FileDescriptor(-1)
181+
print(fd.description)
182+
```
183+
184+
A more general kind of workaround to mix generics and noncopyable types
185+
is to wrap the value in an ordinary class instance, which itself can participate
186+
in generics. The trade-off in doing this is the loss of static guarantees
187+
about the lifetime of the value, since the class reference can be copied.
188+
189+
To transfer the noncopyable value in and out of the wrapper class instance,
190+
introduce a concrete noncopyable enum to represent the case where the value
191+
was taken out of the instance:
192+
193+
```swift
194+
@_moveOnly
195+
enum MaybeFileDescriptor {
196+
case just(FileDescriptor)
197+
case none
198+
}
199+
200+
class WrappedFile {
201+
var file: MaybeFileDescriptor
202+
203+
enum Err: Error { case noFile }
204+
205+
init(_ fd: take FileDescriptor) {
206+
file = .just(fd)
207+
}
208+
209+
func consume() throws -> FileDescriptor {
210+
if case let .just(fd) = file { // consume `self.file`
211+
file = .none // must reinitialize `self.file` before returning
212+
return fd
213+
}
214+
throw Err.noFile
215+
}
216+
}
217+
218+
func example(_ fd1: take FileDescriptor,
219+
_ fd2: take FileDescriptor) -> [WrappedFile] {
220+
// create an array of descriptors
221+
return [WrappedFile(fd1), WrappedFile(fd2)]
222+
}
141223
```
142224

143-
(more examples here)
225+
All of this boilerplate melts away once noncopyable types support generics.
226+
144227

145228
### Using values of `@noncopyable` type
146229

0 commit comments

Comments
 (0)