UniqueBox
01 何が問題だったのか
Swift では、構造体や InlineArray のような値型は通常スタックやレジスタに置かれます。サイズの大きな型を頻繁に受け渡すような場面では、値そのものをコピーして回すよりも、ヒープに一度だけ確保してそのアドレスを介して間接参照した方が、コードサイズや性能の面で有利になることがあります。
ところが、「値をヒープに載せて、スコープを抜けたら自動で解放する」ための安全な道具が標準ライブラリに用意されていませんでした。現状でこれを行うには、UnsafeMutablePointer を直接使う方法があります。
let ptr = unsafe UnsafeMutablePointer<[512 of Int]>.allocate(capacity: 1)
defer {
unsafe ptr.deinitialize()
unsafe ptr.deallocate()
}
unsafe ptr.initialize(to: [...])
unsafe ptr.pointee[15] += 123
このやり方は、初期化・解放の順序を手で揃える必要があり、unsafe な API として間違いやすく危険です。
もうひとつの定石は、クラスを使って値を包むやり方です。
class Box<T> {
var value: T
init(value: T) {
self.value = value
}
}
こちらは安全ですが、クラスは参照カウントを伴う共有所有(std::shared_ptr 相当)のセマンティクスを持つため、「ヒープに値を1つだけ置いて一意に所有したい」というモチベーションには合いません。クラス固有のオーバーヘッドも乗ります。
~Copyable によって struct にも deinit を書けるようになった現在、「ユニーク所有のヒープボックス」を安全な値型として標準ライブラリに用意できるようになりました。
02 どのように解決されるのか
標準ライブラリに、ヒープ上の値を一意に所有するスマートポインタ型 UniqueBox を追加します。~Copyable な値型で、スコープを抜けると確保したヒープ領域が自動で解放されます。C++ の std::unique_ptr、Rust の Box に相当するイメージです。
基本的な使い方
UniqueBox は初期値を渡して初期化し、value プロパティ経由で中身にアクセスします。
var box = UniqueBox<[3 of _]>([1, 2, 3])
print(box.value) // [1, 2, 3]
box.value.swapAt(0, 2)
print(box.value) // [3, 2, 1]
box の寿命が尽きる(スコープを抜ける、あるいは consume される)と、中の値の deinit が呼ばれたうえでヒープ領域が解放されます。
struct Foo: ~Copyable {
func bar() {
print("bar")
}
deinit {
print("foo")
}
}
func main() {
let box = UniqueBox(Foo())
box.value.bar() // "bar"
print("baz") // "baz"
// "foo"
}
API
UniqueBox は ~Copyable で、Value 自体も ~Copyable を許容します。
public struct UniqueBox<Value: ~Copyable>: ~Copyable {
public init(_ initialValue: consuming Value)
}
extension UniqueBox: Sendable where Value: Sendable & ~Copyable {}
extension UniqueBox where Value: ~Copyable {
public var value: Value {
borrow
mutate
}
public consuming func consume() -> Value
}
extension UniqueBox where Value: ~Copyable {
public var span: Span<Value> { get }
public var mutableSpan: MutableSpan<Value> { mutating get }
}
extension UniqueBox where Value: Copyable {
public func clone() -> UniqueBox<Value>
}
value は borrow / mutate アクセサを持つプロパティで、ボックス越しに中の値へそのまま読み書きができます。consume() はボックスを消費し、中の値を取り出して返します(取り出した時点でヒープも解放されます)。span / mutableSpan は、中の値を1要素の Span / MutableSpan として見るためのプロパティで、ポインタ相当の窓口になります。
Value が Copyable のときだけ使える clone() は、ヒープ領域を新しく確保したうえで中身をコピーし、別の UniqueBox を返します。UniqueBox 自体は ~Copyable であり暗黙のコピーはされないため、複製が必要なときは明示的に clone() を呼ぶ設計です。
Sendable 適合は Value: Sendable のときだけ有効なので、Sendable な値を包めば UniqueBox ごと actor 間を渡せます。
安定したアドレス
UniqueBox は、中の値に対して安定したアドレスを保証します。UniqueBox そのものはコンパイラによって自由にムーブされ得ますが、ムーブされても内部で確保しているヒープ領域のアドレスは変わりません。この性質のおかげで、value や span 経由で得たビューは UniqueBox が動いても指したままにできます。
なお、このヒープ領域の生ポインタを取り出す API は今回のスコープには含まれず、future direction として残されています。
今後の展望
今回は「一意所有」のみを対象としています。今後の方向性として、次のような拡張が言及されています。いずれも別提案で検討される想定で、実現を約束するものではありません。
- 参照カウント付きの共有スマートポインタ(
std::shared_ptr/ Rust のArc相当)。~Copyableな値をCopyableに持ち上げるための道具として位置付けられます。 - 明示的な複製のための
Clonable相当のプロトコル。現状clone()はValue: Copyableのときにしか使えませんが、Clonableを介すればUniqueBox<UniqueBox<T>>のような入れ子もclone()で複製できるようになります。 - 安定アドレスを利用した、生ポインタや排他ミュータブル参照(
leakのような API)の提供。C API との連携を視野に入れたものです。 - Rust の
Derefに相当する仕組み。box.value.foo()と書く代わりにbox.foo()と書けるようにして、UniqueBoxを「値の場所を選ぶためだけの薄いラッパ」に近づけるアイデアです。