|
| 1 | +# Box |
| 2 | + |
| 3 | +* Proposal: [SE-NNNN](NNNN-filename.md) |
| 4 | +* Authors: [Alejandro Alonso](https://github.com/Azoy) |
| 5 | +* Review Manager: TBD |
| 6 | +* Status: **Awaiting implementation** |
| 7 | +* Implementation: [swiftlang/swift#NNNNN](https://github.com/swiftlang/swift/pull/NNNNN) |
| 8 | +* Review: ([pitch](https://forums.swift.org/...)) |
| 9 | + |
| 10 | +## Introduction |
| 11 | + |
| 12 | +We propose to introduce a new type in the standard library `Box` which is a |
| 13 | +simple smart pointer type that uniquely owns a value on the heap. |
| 14 | + |
| 15 | +## Motivation |
| 16 | + |
| 17 | +Sometimes in Swift it's necessary to manually put something on the heap when it |
| 18 | +normally wouldn't be located there. Types such as `Int`, `InlineArray`, custom |
| 19 | +struct types, etc. are all typically inline allocated meaning that they may |
| 20 | +exist in registers if it's just local data or maybe on the stack. |
| 21 | + |
| 22 | +A common way of doing something like this today is by using pointers directly: |
| 23 | + |
| 24 | +```swift |
| 25 | +let ptr = unsafe UnsafeMutablePointer<Int>.allocate(capacity: 1) |
| 26 | + |
| 27 | +// Make sure to perform cleanup at the end of the scope! |
| 28 | +defer { |
| 29 | + unsafe ptr.deinitialize() |
| 30 | + unsafe ptr.deallocate() |
| 31 | +} |
| 32 | + |
| 33 | + |
| 34 | +// Must initialize the pointer the first |
| 35 | +unsafe ptr.initialize(to: 123) |
| 36 | + |
| 37 | +// Now we can access 'pointee' and read/write to our 'Int' |
| 38 | +unsafe ptr.pointee += 321 |
| 39 | + |
| 40 | +... |
| 41 | + |
| 42 | +// 'ptr' gets deallocated here |
| 43 | +``` |
| 44 | + |
| 45 | +Using pointers like this is extremely unsafe and error prone. We previously |
| 46 | +couldn't make wrapper types over this pattern because we couldn't perform the |
| 47 | +cleanup happening in the `defer` in structs. It wasn't until recently that we |
| 48 | +gained noncopyable types that allowed us to have `deinit`s in structs. A common |
| 49 | +pattern in Swift is to instead wrap an instance in a class to get similar |
| 50 | +cleanup behavior: |
| 51 | + |
| 52 | +```swift |
| 53 | +class Box<T> { |
| 54 | + var value: T |
| 55 | + |
| 56 | + init(value: T) { |
| 57 | + self.value = value |
| 58 | + } |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +This is much nicer and safer than the original pointer code, however this |
| 63 | +construct is more of a shared pointer than a unique one. Classes also come with |
| 64 | +their own overhead on top of the pointer allocation leading to slightly worse |
| 65 | +performance than using pointers directly. |
| 66 | + |
| 67 | +## Proposed solution |
| 68 | + |
| 69 | +The standard library will add a new noncopyable type `Box` which is a safe smart |
| 70 | +pointer that uniquely owns some instance on the heap. |
| 71 | + |
| 72 | +```swift |
| 73 | +// Storing an inline array on the heap |
| 74 | +var box = Box<[3 of _]>([1, 2, 3]) |
| 75 | + |
| 76 | +print(box[]) // [1, 2, 3] |
| 77 | + |
| 78 | +box[].swapAt(0, 2) |
| 79 | + |
| 80 | +print(box[]) // [3, 2, 1] |
| 81 | +``` |
| 82 | + |
| 83 | +It's smart because it will automatically clean up the heap allocation when the |
| 84 | +box is no longer being used: |
| 85 | + |
| 86 | +```swift |
| 87 | +struct Foo: ~Copyable { |
| 88 | + func bar() { |
| 89 | + print("bar") |
| 90 | + } |
| 91 | + |
| 92 | + deinit { |
| 93 | + print("foo") |
| 94 | + } |
| 95 | +} |
| 96 | + |
| 97 | +func main() { |
| 98 | + let box = Box(Foo()) |
| 99 | + |
| 100 | + box[].bar() // "bar" |
| 101 | + |
| 102 | + print("baz") // "baz" |
| 103 | + |
| 104 | + // "foo" |
| 105 | +} |
| 106 | +``` |
| 107 | + |
| 108 | +## Detailed design |
| 109 | + |
| 110 | +```swift |
| 111 | +/// A smart pointer type that uniquely owns an instance of `Value` on the heap. |
| 112 | +public struct Box<Value: ~Copyable>: ~Copyable { |
| 113 | + /// Initializes a value of this box with the given initial value. |
| 114 | + /// |
| 115 | + /// - Parameter initialValue: The initial value to initialize the box with. |
| 116 | + public init(_ initialValue: consuming Value) |
| 117 | +} |
| 118 | + |
| 119 | +extension Box where Value: ~Copyable { |
| 120 | + /// Consumes the box and returns the instance of `Value` that was within the |
| 121 | + /// box. |
| 122 | + public consuming func consume() -> Value |
| 123 | + |
| 124 | + /// Dereferences the box allowing for in-place reads and writes to the stored |
| 125 | + /// `Value`. |
| 126 | + public subscript() -> Value |
| 127 | +} |
| 128 | + |
| 129 | +extension Box where Value: ~Copyable { |
| 130 | + /// Returns a single element span reference to the instance of `Value` stored |
| 131 | + /// within this box. |
| 132 | + public var span: Span<Value> { |
| 133 | + get |
| 134 | + } |
| 135 | + |
| 136 | + /// Returns a single element mutable span reference to the instance of `Value` |
| 137 | + /// stored within this box. |
| 138 | + public var mutableSpan: MutableSpan<Value> { |
| 139 | + mutating get |
| 140 | + } |
| 141 | +} |
| 142 | + |
| 143 | +extension Box where Value: Copyable { |
| 144 | + /// Copies the value within the box and returns it in a new box instance. |
| 145 | + public func clone() -> Box<Value> |
| 146 | +} |
| 147 | +``` |
| 148 | + |
| 149 | +## Source compatibility |
| 150 | + |
| 151 | +`Box` is a brand new type in the standard library, so source should still be |
| 152 | +compatible. |
| 153 | + |
| 154 | +Given the name of this type however, we foresee this clashing with existing user |
| 155 | +defined types named `Box`. This isn't a particular issue though because the |
| 156 | +standard library has special shadowing rules which prefer user defined types by |
| 157 | +default. Which means in user code with a custom `Box` type, that type will |
| 158 | +always be preferred over the standard library's `Swift.Box`. |
| 159 | + |
| 160 | +## ABI compatibility |
| 161 | + |
| 162 | +The API introduced in this proposal is purely additive to the standard library's |
| 163 | +ABI; thus existing ABI is compatible. |
| 164 | + |
| 165 | +## Implications on adoption |
| 166 | + |
| 167 | +`Box` is a new type within the standard library and currently there are no ways |
| 168 | +to back deploy new types, so adopters must use at least the version of Swift |
| 169 | +that introduced this type. |
| 170 | + |
| 171 | +## Alternatives considered |
| 172 | + |
| 173 | +### Name this type `Unique` |
| 174 | + |
| 175 | +Following C++'s `std::unique_ptr`, a natural name for this type could be |
| 176 | +`Unique`. However, there's a strong precedent of Swift developers reaching for |
| 177 | +the `Box` name to manually put something on the heap. While C++ uses |
| 178 | +`std::shared_ptr`, Rust does name their unique smart pointer type `Box`, so |
| 179 | +there is prior art for both potential names. This proposal suggests `Box` as the |
| 180 | +succinct and simple name. |
| 181 | + |
| 182 | +### Use a named property for dereferencing instead of an empty subscript |
| 183 | + |
| 184 | +```swift |
| 185 | +var box = Box<[3 of _]>([1, 2, 3]) |
| 186 | +box[].swapAt(0, 2) // [3, 2, 1] |
| 187 | +``` |
| 188 | + |
| 189 | +The use of the empty subscript is unprecedented in the standard library or quite |
| 190 | +frankly in Swift in general. We could instead follow in `UnsafePointer`'s |
| 191 | +footsteps with `pointee` or something similar like `value` so that the example |
| 192 | +above becomes: |
| 193 | + |
| 194 | +```swift |
| 195 | +box.pointee.swapAt(0, 2) // [3, 2, 1] |
| 196 | + |
| 197 | +// or |
| 198 | + |
| 199 | +box.value.swapAt(0, 2) // [3, 2, 1] |
| 200 | +``` |
| 201 | + |
| 202 | +We propose the empty subscript because alternatives introduce too much ceremony |
| 203 | +around the box itself. `Box` should simply be just a vehicle with which you can |
| 204 | +manually manage where a value lives versus being this cumbersome wrapper type |
| 205 | +you have to plumb through to access the inner value. |
| 206 | + |
| 207 | +Consider uses of `std::unique_ptr`: |
| 208 | + |
| 209 | +```cpp |
| 210 | +class A { |
| 211 | +public: |
| 212 | + void foo() const { |
| 213 | + std::println("Foo"); |
| 214 | + } |
| 215 | +}; |
| 216 | + |
| 217 | +int main() { |
| 218 | + auto a = std::make_unique<A>(); |
| 219 | + |
| 220 | + a->foo(); |
| 221 | +} |
| 222 | +``` |
| 223 | +
|
| 224 | +All members of the boxed instance can be referenced through C++'s usual arrow |
| 225 | +operator `->` behaving exactly like some `A*`. C++ is able to do this via their |
| 226 | +operator overloading of `->`. |
| 227 | +
|
| 228 | +Taking a look at Rust too: |
| 229 | +
|
| 230 | +```rust |
| 231 | +struct A {} |
| 232 | +
|
| 233 | +impl A { |
| 234 | + fn foo() { |
| 235 | + println!("foo"); |
| 236 | + } |
| 237 | +} |
| 238 | +
|
| 239 | +fn main() { |
| 240 | + let a = Box::new(A {}); |
| 241 | +
|
| 242 | + a.foo(); |
| 243 | +} |
| 244 | +``` |
| 245 | + |
| 246 | +there's no ceremony at all and all members of `A` are immediately accessible |
| 247 | +from the box itself. |
| 248 | + |
| 249 | +Rust achieves this with a special `Deref` trait (or protocol in Swift terms) |
| 250 | +that I'll discuss as a potential future direction. |
| 251 | + |
| 252 | +### Rename `consume` to `take` or `move` |
| 253 | + |
| 254 | +There a plenty of good names that could be used here like `take` or `move`. |
| 255 | +`take` comes from `Optional.take()` and `move` from `UnsafeMutablePointer.move()`. |
| 256 | +Both of those methods don't actually consume the parent instance they are called |
| 257 | +on however unlike `consume`. Calling `consume` ends the lifetime of `Box` and it |
| 258 | +is immediately deallocated after returning the instance stored within. |
| 259 | + |
| 260 | +## Future directions |
| 261 | + |
| 262 | +### Add a `std::shared_ptr` alternative |
| 263 | + |
| 264 | +This proposal only introduces the uniquely owned smart pointer type, but there's |
| 265 | +also the shared smart pointer construct. C++ has this with `std::shared_ptr` and |
| 266 | +Rust calls theirs `Arc`. While the unique pointer is able to make copyable types |
| 267 | +noncopyable, the shared pointer is able to make noncopyable types into |
| 268 | +copyable ones by keeping track of a reference count similar to classes in Swift. |
| 269 | + |
| 270 | +### Introduce a `Clonable` protocol |
| 271 | + |
| 272 | +`Box` comes with a `clone` method that will effectively copy the box and its |
| 273 | +contents entirely returning a new instance of it. We can't make `Box` a copyable |
| 274 | +type because we need to be able to customize deinitialization and for |
| 275 | +performance reasons wouldn't want the compiler to implicitly add copies of it |
| 276 | +either. So `Box` is a noncopyable type, but when its contents are copyable we |
| 277 | +can add explicit ways to copy the box into a new allocation. |
| 278 | + |
| 279 | +`Box.clone()` is only available when the underlying `Value` is `Copyable`, but |
| 280 | +there is a theoretical other protocol that this is relying on which is |
| 281 | +`Clonable`. `Box` itself can conform to `Clonable` by providing the explicit |
| 282 | +`clone()` operation, but itself not being `Copyable`. If this method were |
| 283 | +conditional on `Value: Clonable`, then you could call `clone()` on a box of a |
| 284 | +box (`Box<Box<T>>`). |
| 285 | + |
| 286 | +Rust has a hierarchy very similar to this: |
| 287 | + |
| 288 | +```swift |
| 289 | +public protocol Clonable { |
| 290 | + func clone() -> Self |
| 291 | +} |
| 292 | + |
| 293 | +public protocol Copyable: Clonable {} |
| 294 | +``` |
| 295 | + |
| 296 | +where conforming to `Copyable` allows the compiler to implicitly add copies |
| 297 | +where needed. |
| 298 | + |
| 299 | +### Implement something similar to Rust's `Deref` trait |
| 300 | + |
| 301 | +`Deref` is a trait (protocol) in Rust that allows types to access members of |
| 302 | +another unrelated type if it's able to produce a borrow (or a safe pointer) of |
| 303 | +said unrelated type. Here's what `Deref` looks like in Rust: |
| 304 | + |
| 305 | +```rust |
| 306 | +pub trait Deref { |
| 307 | + type Target: ?Sized; |
| 308 | + |
| 309 | + fn deref(&self) -> &Self::Target; |
| 310 | +} |
| 311 | +``` |
| 312 | + |
| 313 | +It's a very simple protocol that defines an associated type `Target` (let's |
| 314 | +ignore the `?Sized`) with a method requirement `deref` that must return a borrow |
| 315 | +of `Target`. |
| 316 | + |
| 317 | +This trait is known to the Rust compiler to achieve these special |
| 318 | +semantics, but Swift could very well define a protocol very similar to this |
| 319 | +today. While we don't have borrows, I don't believe this protocol definition in |
| 320 | +Swift would require it either. |
| 321 | + |
| 322 | +Without getting too into specifics, array like data structures in Rust also |
| 323 | +conform to `Deref` with their `Target` being a type similar to `Span<Element>`. |
| 324 | +This lets them put shared functionality all on `Span` while leaving data |
| 325 | +structure specific behaviors on the data structure itself. E.g. `swap` is |
| 326 | +implemented on `MutableSpan<Element>` and not `Array` in Swift terms. This is |
| 327 | +being mentioned because if we wanted to do something similar for our array like |
| 328 | +types, they wouldn't want to return a borrow of `Span`, but instead the span |
| 329 | +directly. Rust does this because their span type is spelt `&[T]` while the `[T]` |
| 330 | +is an unsized (hence the `?Sized` for `Target`), so returning a borrow of `[T]` |
| 331 | +naturally leads them to returning `&[T]` (span) directly. |
| 332 | + |
| 333 | +It could look something like the following in Swift for `Box`: |
| 334 | + |
| 335 | +```swift |
| 336 | +public protocol Deref { |
| 337 | + associatedtype Target |
| 338 | + |
| 339 | + subscript() -> Target { ... } |
| 340 | +} |
| 341 | + |
| 342 | +extension Box: Deref where Value: ~Copyable { |
| 343 | + subscript() -> Target { ... } |
| 344 | +} |
| 345 | +``` |
| 346 | + |
| 347 | +which could allow for call sites to look like the following: |
| 348 | + |
| 349 | +```swift |
| 350 | +var box = Box<[3 of _]>([1, 2, 3]) |
| 351 | +box.swapAt(0, 2) // [3, 2, 1] |
| 352 | +``` |
0 commit comments