Swift Digest
SE-0136 | Swift Evolution

Memory layout of values

Proposal
SE-0136
Authors
Xiaodi Wu
Review Manager
Dave Abrahams
Status
Implemented (Swift 3.0)

01 何が問題だったのか

SE-0101 で、Swift 2 まで存在していた 6 つのフリー関数(sizeof / sizeofValue / strideof / strideofValue / alignof / alignofValue)は、ジェネリックな構造体 MemoryLayout<T> の static プロパティに集約されました。これにあわせて、値を受け取る sizeofValue(_:) / strideofValue(_:) / alignofValue(_:) は置き換え先を用意せずに削除されました。

サイズは型の性質であって個々の値の性質ではない、というのが SE-0101 の立場だったため、値版は型ベースの呼び出しで書き換えれば十分だと考えられていました。

値からレイアウトを取り出す手段が失われた

ところが実装後に、値版の欠落が標準ライブラリや周辺コードに無視できない影響を与えていることが分かりました。具体的には、ジェネリックな文脈で「いま手元にある値と同じ型」のサイズやストライドを取りたい場面で、代わりの書き方が無いという問題です。

func writeBytes<T>(of value: T) {
    // ここで T のサイズが欲しい
    // Swift 2 の書き方: sizeofValue(value)
    // SE-0101 後の書き方: MemoryLayout<T>.size でよいが、
    // T が推論任せのときや、値が手元にあって型名を書きたくないときは不便
}

さらに、静的な型と動的な型が異なるケースを type(of:) と組み合わせて表現しようとすると、意図とずれた結果になってしまいます。

protocol P {}
extension Int: P {}
var x: P = 1

// type(of: x) は動的には Int.self を返すが、
// 静的には P.Type として扱われるため、
// MemoryLayout.of(type(of: x)).size のような書き方は
// Int ではなく existential box のサイズを計算してしまう

標準ライブラリ内でも underscored API に頼っていた

代替 API が無いため、標準ライブラリ内部では MemoryLayout._ofInstance(x).size というアンダースコア付きの非公開 API を使って回避していました。公開 API から到達できない道具で内部を支えている状態は、SE-0101 の削除が実装に悪影響を与えていたことのはっきりした兆候でした。

値版を不用意に戻すとまた同じ議論を蒸し返すことになるため、Swift 3 のバグフィックスの範囲でどのように最小限の形で戻すか、という設計が必要でした。

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

値からメモリレイアウトを取り出す API を、MemoryLayout の static メソッドとして復活させます。Swift 2 時代のような独立したフリー関数には戻さず、SE-0101 で整えた名前空間の中に収める形です。

MemoryLayout に 3 つの static メソッドを追加

MemoryLayout に、値を受け取ってサイズ・ストライド・アラインメントを返す 3 つの static メソッドを用意します。

extension MemoryLayout {
    public static func size(ofValue _: T) -> Int {
        return MemoryLayout.size
    }
    public static func stride(ofValue _: T) -> Int {
        return MemoryLayout.stride
    }
    public static func alignment(ofValue _: T) -> Int {
        return MemoryLayout.alignment
    }
}

使う側は、値を引数に渡すだけでレイアウト情報を取得できます。

let x: UInt8 = 5
MemoryLayout.size(ofValue: x)       // 1
MemoryLayout.stride(ofValue: x)     // 1
MemoryLayout.alignment(ofValue: x)  // 1

func writeBytes<T>(of value: T) {
    let size = MemoryLayout.size(ofValue: value)
    // ...
}

返される値は、あくまで引数の 静的な型 のレイアウトです。sizeofValue と同様に、Array のように内部バッファを持つ型でもバッファ分のメモリは含まれませんし、プロトコル型の変数に入っている動的な値の実型のサイズでもありません。この点は Swift 2 時代の sizeofValue(_:) とまったく同じ挙動です。

引数は通常どおり評価される

MemoryLayout.size(ofValue: iter.next()) のように、副作用のある式を渡した場合は、その式は普通に評価されます。@autoclosure を付けて評価を避ける案も検討されましたが、標準ライブラリに前例が無く、foo(bar(x)) と書いたら bar(_:)foo(_:) も呼ばれるという Swift の一般的な期待からも外れるため、採用されていません。

既存コードの移行

Swift 2 の sizeofValue(x) / strideofValue(x) / alignofValue(x) は、この提案の API でそのまま置き換えられます。

let x: UInt8 = 5

// Before (Swift 2)
sizeofValue(x)
strideofValue(x)
alignofValue(x)

// After (Swift 3)
MemoryLayout.size(ofValue: x)
MemoryLayout.stride(ofValue: x)
MemoryLayout.alignment(ofValue: x)

標準ライブラリ内部で MemoryLayout._ofInstance(x).size のように使われていた箇所も、この公開 API に置き換えられます。SE-0101 で削除した値版を、名前空間を整えたうえで必要最小限の形で取り戻すバグフィックスです。