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 として残されています。
03 今後の見通し
今回は「一意所有」のスマートポインタのみを対象としていますが、Proposal では今後の方向性として次のような拡張が挙げられています。いずれも将来の構想であり、実現を約束するものではありません。
共有スマートポインタの追加
参照カウント付きの共有スマートポインタ(C++ の std::shared_ptr や Rust の Arc に相当する型)の追加が検討されています。UniqueBox が Copyable な値を non-copyable として扱う方向の道具であるのに対し、共有スマートポインタは non-copyable な値を Copyable に持ち上げるための道具として位置付けられます。
Clonable 相当のプロトコル
現状の clone() は Value: Copyable のときにしか使えませんが、明示的な複製のための Clonable 相当のプロトコルを導入する案が示されています。UniqueBox 自身を Clonable に適合させ、clone() を Value: Clonable で条件付けすれば、UniqueBox<UniqueBox<T>> のような入れ子も clone() で複製できるようになります。Rust では Copyable が Clonable を継承する階層を持っており、その方向性が参照されています。
生ポインタを取り出す API
UniqueBox は中の値に対して安定したアドレスを保証するため、unsafeAddress() / unsafeMutableAddress() のような生ポインタを返す API を追加することは自然な拡張です。一方で、新しい言語機能と組み合わせて、ボックスを消費して排他ミュータブル参照を返す leak のような API(仮称)を提供する方向も挙げられており、両者を併せて検討するために今回は見送られています。
Rust の Deref に相当する仕組み
box.value.foo() と書く代わりに box.foo() と書けるようにする、Rust の Deref トレイトに相当する仕組みも検討されています。これにより UniqueBox を、値をヒープ上に置くための薄いラッパとして、ラッパ越しのアクセスを意識せずに使えるようになります。Swift には借用の概念がまだ十分には揃っていませんが、borrow アクセサを返す associated type を持つプロトコルとして定義する形が示されています。