withUnsafePointer(to:_:) and withUnsafeBytes(of:_:) for immutable values
01 何が問題だったのか
トップレベルの withUnsafePointer(to:_:) と withUnsafeBytes(of:_:) は、値のメモリ表現を指すポインタをクロージャに一時的に渡すAPIです。しかしSwift 4.1までは、これらは第1引数を inout でしか受け取れませんでした。そのため、let で宣言したイミュータブルな値や式の結果など、書き換える必要のない値に対してポインタを取りたいだけでも、わざわざ var にコピーしてから & を付けて渡す必要があり、コードが冗長になるうえに不要なコピーまで発生してしまいました。
let x = 1 + 2
var x2 = x // inout に渡すためだけのコピー
withUnsafeBytes(of: &x2) { bytes in
// ...
}
一方で、withUnsafePointer / withUnsafeBytes の意味論はもともと非常に限定的です。渡されるポインタは body クロージャの実行中しか有効でなく、クロージャを抜けたあとに使うことはできず、ポインタを経由して値を書き換えることも禁止されていて、同じプロパティに対して繰り返し呼び出しても同じアドレスが返ってくる保証すらありません。要するに「クロージャ実行中のあいだだけ、読み取り専用のポインタとしてメモリ表現を覗かせる」だけの機能なので、本来 inout である必要はなく、読み取り専用の値に対しても提供できるはずでした。実際、この機能を let や一時値にも使いたいという要望は繰り返し上がっていました。
02 どのように解決されるのか
標準ライブラリに、第1引数を値(読み取り専用)で受け取る withUnsafePointer(to:_:) と withUnsafeBytes(of:_:) のオーバーロードを追加します。既存の inout 版はそのまま残るため、用途に応じて使い分けられます。
追加されるオーバーロード
次のトップレベル関数がstdlibに追加されます。
public func withUnsafePointer<T, Result>(
to value: T,
_ body: (UnsafePointer<T>) throws -> Result
) rethrows -> Result
public func withUnsafeBytes<T, Result>(
of value: T,
_ body: (UnsafeRawBufferPointer) throws -> Result
) rethrows -> Result
挙動は inout 版と同じで、value のメモリ表現を指すポインタを生成し、それを body に渡します。ポインタが有効なのは body の実行中だけで、戻ってから触るのは未定義動作です。ポインタ経由の書き込みも未定義動作で、同じ変数・プロパティに対して withUnsafePointer / withUnsafeBytes を複数回呼び出しても、毎回同じアドレスが返る保証はありません。
これにより、let や式の結果に対しても、追加の var コピーなしに直接ポインタを取れるようになります。
let x = 1 + 2
withUnsafeBytes(of: x) { bytes in
// x のメモリ表現を bytes として読み取れる
}
let point = (x: 1.0, y: 2.0)
withUnsafePointer(to: point) { ptr in
// ptr: UnsafePointer<(x: Double, y: Double)>
}
inout 版との関係
inout 版は削除されません。ソース互換性のためというだけでなく、現在のSwiftでは inout が「そのストレージへの排他的アクセスを宣言し、暗黙のコピーを抑制する」ことを確実に表現できる唯一の手段だからです。たとえば let プロパティ x に対して withUnsafePointer(to: x) と書いた場合、最適化されたSwiftでは原則コピーなしで済むはずですが、現状の言語仕様ではそれを静的に保証する仕組みがないため、コピーが入る可能性は残ります。確実にコピーを避けたい場面では、引き続き inout 版を var に対して使うのが安全です。
将来のmoveonly型との整合性
この追加は、将来導入される可能性のあるmoveonly型への布石でもあります。moveonly型が入れば、値版の引数は「shared borrow(共有借用)」として渡される形に自然に対応づけられ、コピーが言語レベルで回避されるようになります。今回のAPIは、そうなったときに ABI 互換を保てるよう、引数を +0(所有権を移さない形)で受け取る実装にしておくことが前提になっています。今のところは speculative な見通しですが、APIの形としては将来の改善を素直に取り込めるよう設計されています。