Swift Digest
SE-0370 | Swift Evolution

Pointer Family Initialization Improvements and Better Buffer Slices

Proposal
SE-0370
Authors
Guillaume Lessard
Review Manager
John McCall
Status
Implemented (Swift 5.8)

01 何が問題だったのか

UnsafeMutablePointer ファミリでは、メモリの確保・解放だけでなく、各領域が「初期化済みか未初期化か」という初期化状態(initialization state)もプログラマ自身が管理する必要があります。ところが、この状態遷移を扱うための API が UnsafeMutablePointer にはそろっている一方で、UnsafeMutableRawPointerUnsafeMutableBufferPointerUnsafeMutableRawBufferPointer、およびこれらのバッファのスライス(Slice<UnsafeMutableBufferPointer> / Slice<UnsafeMutableRawBufferPointer>)では不足しており、部分的に初期化されたバッファを扱う場面で不必要に煩雑なコードを書く必要がありました。

たとえば RangeReplaceableCollection.replaceSubrange(_:with:) のように、バッファの途中に要素を挿入する実装を書こうとすると、バッファの末尾部分を後ろにずらす操作、既存領域の値を書き換える操作、隙間に新しい要素を書き込む操作が入り混じります。従来はこれらをバッファのまま行う手段が乏しく、いったん UnsafeMutablePointer に取り出して要素ごとにループを回し、境界チェックも自前で行う必要がありました。

// 末尾領域を後ろに move-initialize
newTailBase.moveInitialize(from: oldTailBase,
                           count: oldCount - subrange.upperBound)

// 既存領域の値を 1 要素ずつ書き換え
var j = newElements.startIndex
for i in subrange {
    buffer[i] = newElements[j]
    newElements.formIndex(after: &j)
}
// 隙間を 1 要素ずつ initialize
for i in subrange.upperBound..<newTail.lowerBound {
    buffer.baseAddress!.advanced(by: i).initialize(to: newElements[j])
    newElements.formIndex(after: &j)
}

また、既存 API では「初期化済みの領域を更新する」操作が assign という名前になっていました。assign は TSPL における代入演算子 = の説明で「初期化(initialize)」と「更新(update)」の両方を兼ねており、未初期化メモリに assign を誤って呼び出してしまうバグを招きがちでした。さらに、initialize(from:) のような Sequence を受け取る API は「バッファに十分な容量がある」という前提を文書で述べてはいたものの、実際に強制する手段がなく、挙動が緩くなっていました。

02 どのように解決されるのか

UnsafeMutableBufferPointerUnsafeMutableRawBufferPointer、およびこれらのスライス、さらに UnsafeMutableRawPointer / UnsafeMutablePointer に対して、初期化状態を扱う API が整備されました。大きな方針は次の3点です。

  • 既存の assign 系メソッドを update にリネームし、「初期化済みのメモリを更新する」という意図を名前で表す。
  • Collection を受け取る fromContentsOf: 版を追加し、「コピー先に十分な容量があること」を事前条件として厳密に要求する。容量が足りない場合はトラップし、戻り値は「最後に書き込んだ次の位置」を表す Index を返す。
  • バッファのスライスにも、ベースと同じ BufferPointer 向けメソッドを用意し、バッファの部分範囲に対して直接操作できるようにする。

assign から update へのリネーム

UnsafeMutablePointerUnsafeMutableBufferPointerassign(repeating:) / assign(from:) / moveAssign(from:)update(repeating:) / update(from:) / moveUpdate(from:) に改名されました。旧名は deprecated となり、fix-it によって新しい名前へ置き換えられます。新しく追加されるメソッドも同じ流儀で update を使います。

fromContentsOf: による Collection コピー

UnsafeMutableBufferPointer には、Collection を受け取る版が追加されました。

extension UnsafeMutableBufferPointer {
    func initialize<C: Collection>(fromContentsOf source: C) -> Index
      where C.Element == Element
    func update<C: Collection>(fromContentsOf source: C) -> Index
      where C.Element == Element
    func moveInitialize(fromContentsOf source: Self) -> Index
    func moveInitialize(fromContentsOf source: Slice<Self>) -> Index
    func moveUpdate(fromContentsOf source: Self) -> Index
    func moveUpdate(fromContentsOf source: Slice<Self>) -> Index
}

これらはいずれも self.count >= source.count を事前条件とし、満たさない場合はトラップします。戻り値の Index は、source のすべての要素を書き終えた次の位置を指します。従来の Sequence 版(initialize(from:) / update(from:))は残っており、書ききれなかった要素の Iterator と、次の未初期化位置の Index をタプル (unwritten:, index:) として返します。ラベル名はいずれも unwritten / index に揃えられました。

バッファ全体・要素ごとの deinitialize

バッファ全体を一括で未初期化状態に戻す deinitialize() と、1 要素だけ扱う initializeElement(at:to:) / moveElement(from:) / deinitializeElement(at:) が追加されました。これにより、要素単位でもバッファ単位でも、baseAddress を取り出さずに初期化状態を操作できます。なお、「初期化済みの要素を書き換える」操作は従来どおり buffer[index] = value で表現できるため、単一要素の update に相当するメソッドは追加されません。

スライスへの対応

Slice<UnsafeMutableBufferPointer<T>>Slice<UnsafeMutableRawBufferPointer> に、ベースの BufferPointer と同じ API が提供されるようになりました。initialize / update / moveInitialize / moveUpdate / deinitializeinitializeElement / moveElement / deinitializeElement、さらにスライスには withMemoryRebound(to:_:)、raw 側のスライスには bindMemory(to:) / assumingMemoryBound(to:) / load(fromByteOffset:as:) / loadUnaligned(fromByteOffset:as:) / storeBytes(of:toByteOffset:as:) / copyMemory(from:) / copyBytes(from:) / initializeMemory(as:...) / moveInitializeMemory(as:...) などが揃います。境界チェックもスライスのそれに従うため、自前でオフセットを計算する必要がなくなります。

その結果、冒頭の挿入処理は次のように簡潔に書けます。

// 末尾領域をスライスごと後ろに move-initialize
var m = buffer[newTail].moveInitialize(fromContentsOf: buffer[oldTail])

// 既存領域を新要素で更新
m = buffer[subrange].update(fromContentsOf: newElements)

// 残りの隙間を initialize
m = buffer[m..<newTail.lowerBound].initialize(
    fromContentsOf: newElements.dropFirst(m - subrange.lowerBound)
)

生メモリ側の補完

UnsafeMutableRawBufferPointer にも、initializeMemory(as:fromContentsOf:)moveInitializeMemory(as:fromContentsOf:)UnsafeMutableBufferPointer 版・スライス版)が追加されました。UnsafeMutableRawPointer には、単一値を初期化する initializeMemory(as:to:) が追加され、1 要素だけ必要な場面で count: 1 を指定せずに済むようになりました。

let bytePointer = UnsafeMutableRawPointer.allocate(
    byteCount: MemoryLayout<UInt>.stride,
    alignment: MemoryLayout<UInt>.alignment)
let intPointer = bytePointer.initializeMemory(as: UInt.self, to: 0)

移行時の注意

これらのメソッドは @_alwaysEmitIntoClient として実装されているため、古い OS にもバックデプロイされます。assign 系は deprecated となりますが、当面は利用可能で fix-it による自動移行が効きます。Sequence を受け取る initialize(from:) / update(from:) は、返すタプルに (unwritten:, index:) というラベルが付く点が厳密には source-breaking ですが、既存コードへの影響はごく軽微なものに留まります。