Skip to content

Commit da76a65

Browse files
committed
Propose Box
1 parent 225a928 commit da76a65

File tree

1 file changed

+352
-0
lines changed

1 file changed

+352
-0
lines changed

proposals/nnnn-box.md

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
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

Comments
 (0)