素晴らしいSwiftのポインタ型の解説

  • 99
    いいね
  • 0
    コメント

導入

Swiftにはポインタを表すための型として UnsafePointer<T> とその仲間達があります。CoreFoundationなどC言語のライブラリを使う時などに利用することになります。これらのポインタ型のAPIはとてもよく考えられた素晴らしいものです。この記事ではそれを紹介、解説します。C言語ユーザ、C++ユーザにとっても興味深い内容だと思います。(swift 3.0.2)

ポインタ型

ポインタ型には下記のものがあります。

基本ポインタ型

  • UnsafePointer<T>
  • UnsafeMutablePointer<T>
  • UnsafeRawPointer
  • UnsafeMutableRawPointer
  • UnsafeBufferPointer<T>
  • UnsafeMutableBufferPointer<T>
  • UnsafeRawBufferPointer
  • UnsafeMutableRawBufferPointer

言語ブリッジ用

  • OpaquePointer
  • CVaListPointer
  • AutoreleasingUnsafeMutablePointer<T>

やたら多いですね。

基本ポインタ型の属性

最も基本的なポインタ型は UnsafePointer<T> です。T型の値へのポインタを表します。参照先のTの値はイミュータブルになります。 UnsafePointer<T> 自体はstructなので、letvarによってポインタ自体の不変性を表すことができます。

func f(_ x: UnsafePointer<Int>) {
    let a: UnsafePointer<Int> = x
    var b: UnsafePointer<Int> = x
}

上記をC言語で表すと下記のようになります。

void f(const int * const x) {
    const int * const a = x;
    const int * b = x;
}

UnsafePointer<T> に対して、3つの属性が付くことで別の型になります。組み合わせは2x2x2の8通りあります。

Mutability

参照先Tの値が変更可能なミュータブル版があります。名前にはMutableが付きます。イミュータブル版からは mutating ラベル付きのコンストラクタで変換することができます。逆にミュータブル版からイミュータブル版へは、ラベル無しコンストラクタで変換できます。

public struct UnsafePointer<Pointee> : Strideable, Hashable {
    ...
    public init(_ other: UnsafeMutablePointer<Pointee>)
    ...
}

public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
    ...
    public init(mutating other: UnsafePointer<Pointee>)
    ...
}

型付きと型無し

UnsafePointer<T> は参照先の型Tを型パラメータとして持っていましたが、この型情報を持たない版があります。型情報を持たない版の名前には Raw が付きます。例えばC言語での const void * がSwiftからはこの UnsafeRawPointer として見えます。ポインタの参照先の型が不明な時に使います。

元々導入の経緯としては、 UnsafePointer<T> だけではある種のコードでstrict aliasingに関するバグが生じる可能性があり、それを回避するためのmemcpyセマンティクスを持った UnsafeRawPointer が必要となったそうです。

詳しいことは規格提案時の文書に書いてあります。UnsafeRawPointer API

また、strict aliasingについては下記がわかりやすいです。(翻訳)C/C++のStrict Aliasingを理解する または - どうして#$@##@^%コンパイラは僕がしたい事をさせてくれないの!

型付きと型無しの間の変換については後述します。

ポインタとバッファ

C言語では配列を表すためにポインタをよく使いますが、いわゆる配列的に扱うためにはポインタに加えて要素数を一緒に知っておく必要があります。そこでこのポインタと要素数をセットにして表した配列を表すのが UnsafeBufferPointer<T> です。イミュータブルとミュータブル、型付きと型無しに対応して、4種類の Buffer があります。

UnsafeBufferPointer<T> はコンストラクタで開始アドレスと要素数を受け取るようになっています。そして Collection プロトコルを継承しています。

public struct UnsafeBufferPointer<Element> : Indexable, Collection, RandomAccessCollection {
    ...
    public init(start: UnsafePointer<Element>?, count: Int)
    ...
    public var baseAddress: UnsafePointer<Element>? { get }
    ...
    public var count: Int { get }
    ...
}

ブリッジ用の型

OpaquePointer

OpaquePointer はC言語のopaque pointerと呼ばれるパターンの型を表すためのものです。opaque pointerというのは、型の名前だけ前方宣言されているが型の定義が見えていないために、参照先へのアクセスについて実質なんの情報も無いポインタの事です。ライブラリ外部に対して、ライブラリ内部の詳細を隠蔽するために利用されたりします。Swiftでは型の名前だけ見えるが定義は見えない、という事は起こりませんが、C言語上で定義されたopaque pointerをSwiftから読み込んだ時にはそれを表現する必要があるため、この型が使われます。

例えば下記のCソースがあったとします。

struct CatImpl;
struct Cat {
    CatImpl * impl;
}
void PassCat(const Cat * a);
void PassCatImpl(const CatImpl * b);

Swiftからは2つの関数は下記のように見えます。

func PassCat(a: UnsafePointer<Cat>?)
func PassCatImpl(b: OpaquePointer?)

UnsafePointer<T> と相互にコンストラクタを通じて変換ができます。

public struct UnsafePointer<Pointee> : Strideable, Hashable {
    ...
    public init(_ from: OpaquePointer)
    ...
}

public struct OpaquePointer : Hashable {
    ...
    public init<T>(_ from: UnsafePointer<T>)
    ...
}

CVaListPointer

C言語で可変長引数を扱う時には、 ... という特殊表記や va_list という特殊な型を使いますが、 va_list を扱うためのポインタが CVaListPointer です。

AutoreleasingUnsafeMutablePointer

調べられていません。Objective-Cにおいて __autoreleasing 修飾子がついたポインタをSwiftから見るとこれになる気がしますがわかりません。

Optionalとポインタとnullability

Swiftのポインタ型はNULLになりません。言い換えると、 UnsafePointer<T> は non-null なポインタです。nullableなポインタはOptionalを使って UnsafePointer<T>? として表します。 Swiftではポインタの取扱においても、 if let 構文などの null安全機構が使えるのです。

UnsafePointer<T>UnsafeMutablePointer<T>OpaquePointer の間での変換コンストラクタについては、Optional版があり、引数としてnilが渡されるとコンストラクタもnilを返します。

public struct UnsafePointer<Pointee> : Strideable, Hashable {
    ...
    public init?(_ from: OpaquePointer?)
    ...
    public init?(_ other: UnsafeMutablePointer<Pointee>?)
    ...
}

UnsafeBufferPointer<T> はそのままの型で nullableポインタです。コンストラクタで受けるポインタが最初からOptionalで受けていて、そこにnilを渡すと、 baseAddress プロパティがnilになります。

基本的なアクセス

UnsafePointer<T> の参照先は pointee プロパティでアクセスできます。C言語では デリファレンス演算子(*)や アロー演算子(->) で書いていたやつです。また、subscriptアクセスができるので、連続確保したメモリ領域を配列のようにアクセスできます。

public struct UnsafePointer<Pointee> : Strideable, Hashable {
    ...
    public var pointee: Pointee { get }
    ...
    public subscript(i: Int) -> Pointee { get }
    ...
}

ミュータブル版だとこれが書き込み可能になります。

public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
    ...
    public var pointee: Pointee { get nonmutating set }
    ...
    public subscript(i: Int) -> Pointee { get nonmutating set }
    ...
}

型無しのRaw版にはこれらのプロパティはありません。型を割り当てないと参照先にはアクセスできないようになっています。

ポインタの3つの状態

UnsafePointer<T> が指し示すメモリには状態が3つあります。

  • not allocated
  • allocated but uninitialized
  • initialized

この3つの状態の区別が、Swiftのポインタ型を支える重要な概念になります。これらの3つの状態は型システムによっては区別されません。プログラマが自分の今扱っているポインタがどの状態であるかを正確に理解している必要があります。

しかしこれはSwiftによって追加された仕様などではなく、本質的にポインタというものにたいして存在する概念です。これを以下に説明します。

メモリ確保

アロケートされている状態というのは、ポインタの指し示すメモリ領域が確保されているという事です。逆にアロケートされていない状態というのは、ポインタがNULLであるか、指し示すメモリ領域が解放されているという事です。

ここで UnsafePointer<T> が扱うメモリ領域の大きさは、必ずしも値1つ分のサイズとは限りません。複数要素格納できるように直列に確保されたメモリ領域も取り扱えます。

メモリ確保はミュータブル版のポインタのstaticメソッド、 allocate によって行えます。

public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
    ...
    public static func allocate(capacity count: Int) -> UnsafeMutablePointer<Pointee>
    ...
}

引数 count は連続何個分のメモリ領域を確保するかの個数です。このメソッドは値の型 Pointee に応じて、アライメント(alignment)とストライド(stride)が調整されます。アライメントというのは、その値が置かれるメモリアドレスの、アドレスの値に対する整数比の制約です。例えばアライメントが8の場合は、メモリアドレスは必ず8の倍数になります。 MemoryLayout<T>.alignment で取得できます。ストライドというのは、連続確保する時にそれぞれの値がアドレスを何バイトずつズレて配置されるかという値です。例えば、型のメモリサイズが5バイトであっても、ストライドが8バイトである場合、1つの要素のためには8バイトずつ割り当てられ、3バイトずつ空白が空きます。 MemoryLayout<T>.stride で取得できます。これらの値はコンパイラによって決められ、プログラマが任意に定める事ができません。

型無しの UnsafeRawPointer の場合、これらの値がわからないので、 allocate のパラメータが異なります。

public struct UnsafeMutableRawPointer : Strideable, Hashable {
    public static func allocate(bytes size: Int, alignedTo: Int) -> UnsafeMutableRawPointer
}

純粋なバイト数と、アライメント値を指定するようになっています。連続確保したい場合は、先述したストライドを考慮してサイズを計算する必要があります。

アロケートされたメモリ領域を表すポインタは non-null なので、どちらの返り値も 非Optional になっています。また、イミュータブルなメモリ領域を確保しても何も嬉しくないので、これらのstaticメソッドはミュータブル版の型に定義されています。

アロケートされたメモリを解放するには、 deallocate メソッドを呼び出します。

public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
    public func deallocate(capacity: Int)
}

メモリ初期化状態

Initializedというのは、そのメモリ領域に値が存在するかどうかを表す概念です。確保しただけのメモリ領域は本当にただのメモリ領域で、そこに値はまだ存在していない、未初期化な状態です。

初期化済みか未初期化であるかは、そのポインタの参照先にアクセスして値を 読み込む時書き込む時 に問題になります。

未初期化なメモリ領域から値を読み込んでしまえば、メモリ上の状態はどんなメチャクチャなものかわからないので、とんでもない値が入っている可能性があり、クラッシュの危険があります。これはわかりやすいと思います。面白いのは、書き込みに関しての場合です。

一般にSwiftの var で定義する変数について考えてみます。下記のように、 Cat 型(参照型)とそれを保持する CatHouse 型(値型)があったとします。

class Cat {
}

struct CatHouse {
    var cat: Cat?
}

下記のような App 型が、 CatHouse 型のプロパティを持っていて、これを update メソッドの中で書き換えたとします。

class App {
    init (a: CatHouse) {
        self.a = a
    }

    var a: CatHouse

    func update(b: CatHouse) {
        self.a = b
    }
}

この時swiftのARC機構によって b のCatHouseが持っている cat の参照カウンタを1増やす事になりますが、もう一つ忘れてはならないのが、 もともと a に入っていた古いCatHouseが持っていた cat の参照カウンタを1減らす処理が生じる 事です。つまり一般にswiftにおいて値のコピーが生じる時、コピーによって消される 古い値の破壊処理が発生します

さて、ポインタの参照先に値を書き込むときの事を考えてみます。ポインタの参照先に値を書き込む時も、そこに変数があるのと同じことですから、元々あった値の破壊処理が必要になります。しかし、確保したばかりでまだ1度も値を書き込んでいない時はどうでしょうか。その状態で元々あった値の破壊処理をしたらまずいことになります。値が書き込まれていないでたらめなメモリ状態だからです。

そこで初期化・未初期化の区別が必要になります。 UnsafePointer<T>pointee プロパティは、初期化済みのときにしか使ってはいけない規約になっています。未初期化のメモリ領域に書き込むためには initialize メソッド、 初期化済みのメモリ領域を未初期化に戻すためには deinitialize メソッドを使用します。

public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
    ...
    public func initialize(to newValue: Pointee, count: Int = default)
    ...
    public func deinitialize(count: Int = default) -> UnsafeMutableRawPointer
    ...
    public func move() -> Pointee
    ...
}

さて、メモリ領域は複数要素分を確保する事が可能でしたから、これらのメソッドには count 引数があります。 initialize については to 引数で指定した値で、 count 個の要素を埋めます。 この際、元々のメモリ領域に対する破壊処理は行わないわけです。 逆に deinitialize メソッドは値の破壊処理だけを行われます。 move メソッドは、要素数が1個の時の deinitialize で、値を返り値として返してくれます。 ちょうど C++の std::move と同じ名前で、ムーブセマンティクスが実現されている事がわかります。

実験してみます。 Catinitdeinit でログが出るようにします。

class Cat {
    init () {
        print("init")
    }
    deinit {
        print("deinit")
    }
}

そして下記の関数を実行します。

func test1() {
    var p = UnsafeMutablePointer<CatHouse>.allocate(capacity: 1)
    defer {
        p.deallocate(capacity: 1)
    }
    p.initialize(to: CatHouse(cat: Cat()))
    p.move()
}

defer を使って deallocate を前置してみました。出力は下記のようになります。

init
deinit

さて、ここで、 move をしない版を作ってみます。

func test2() {
    var p = UnsafeMutablePointer<CatHouse>.allocate(capacity: 1)
    defer {
        p.deallocate(capacity: 1)
    }
    p.initialize(to: CatHouse(cat: Cat()))
}

すると、deinitがされなくなってしまいました。

init

メモリ領域は解放されたものの、そこに書き込まれていた CatHouse の破壊処理が行われていないため、それが保持していた Cat のカウンタを減らす処理が実行されておらず、メモリリークしてしまったのです。

また、途中で pointee を使って古い値を消すテストをしてみます。

func test3() {
    var p = UnsafeMutablePointer<CatHouse>.allocate(capacity: 1)
    defer {
        p.deallocate(capacity: 1)
    }
    p.initialize(to: CatHouse(cat: Cat()))
    p.pointee = CatHouse(cat: Cat())
    p.move()
}
init
init
deinit
deinit

正しく2回作られ2回削除されているのがわかります。

では試しに、 initialize 前に値を書き込んだらどうなるでしょうか。

func test4() {
    var p = UnsafeMutablePointer<CatHouse>.allocate(capacity: 1)
    defer {
        p.deallocate(capacity: 1)
    }
    p.pointee = CatHouse(cat: Cat())
    p.initialize(to: CatHouse(cat: Cat()))
    p.move()
}
init
init
deinit

このように、 Cat が1つメモリリークしてしまいました。 initialize 前に書き込んだ pointee は、 initialize の際に 破壊処理無しで上書きされてしまう ために、 cat のカウンタ操作がスキップされてリークしてしまったのです。

そしてこのコードはそれ以前に、 pointee への書き込み時に、 未初期化領域に対して破壊処理を実行している
ので、クラッシュの危険もあります。

ポインタ間の値のやりとりとムーブセマンティクス

アロケートされたメモリが2つあり、片方が初期化済みだったとします。つまり片方には値が存在したとします。この時、存在する方のポインタからもう片方のポインタへ値を移す時に、下記の条件によって2x2通りのパターンがあります。

  • 送り先のポインタは初期化済みか、未初期化か
  • 送り元のポインタの値はそのままにするか、破棄するか

Swiftでは値型のコピーは高速ですが、例えば CatHouse のようにプロパティとして参照型を持っている場合、コピーする時にその参照のカウンタを1増やすという処理が必要になり、そのオーバーヘッドがあります。もし、コピー元の値をその後で破棄するのであれば、その時にカウンタが1減ることになるので、1増えて1減ることになり無駄です。そこで、コピー元の値を破棄すると同時にコピー先に値を映す、という操作があれば、この無駄なオーバーヘッドを除去することができます。これを C++ ではムーブ操作と言うのですが、Swiftのポインタ型にはこのムーブ用のメソッドがあります。

先述したように、未初期化メモリに値を書き込む操作を initialize と呼びました。一方、初期化済みメモリに値を書き込む操作を assign と言います。これら2つが普通のコピーです。そして、これらのムーブ操作版があり move というprefixが付きます。

public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
    ...
    public func initialize(from source: UnsafePointer<Pointee>, count: Int)
    ...
    public func moveInitialize(from source: UnsafeMutablePointer<Pointee>, count: Int)
    ...
    public func assign(from source: UnsafePointer<Pointee>, count: Int)
    ...
    public func moveAssign(from source: UnsafeMutablePointer<Pointee>, count: Int)
    ...
}

ムーブ版は source がミュータブルになります。破棄操作の対象となるからです。

Raw系については初期化・破棄の制御はできません。型が不明だからです。

メモリ状態とバッファ型

BufferPointer 系の型は確保や初期化などのメソッドは持っていません。これらのメモリ操作はポインタの型によって行うようになっていて、バッファはあくまでそこに対するビューのように機能します。

型有りと無しの間の変換

型有りのポインタ UnsafePointer<T> から 型無しのポインタ UnsafeRawPointer への変換はコンストラクタで可能です。

public struct UnsafeRawPointer : Strideable, Hashable {
    ...
    public init<T>(_ other: UnsafePointer<T>)
    ...
}

しかし、型無しのポインタから型有りへの変換はコンストラクタではできません。代わりに専用のメソッドが2つあります。

public struct UnsafeRawPointer : Strideable, Hashable {
    ...
    public func bindMemory<T>(to type: T.Type, capacity count: Int) -> UnsafePointer<T>
    ...
    public func assumingMemoryBound<T>(to: T.Type) -> UnsafePointer<T>
    ...
}

どうやら先述したstrict-aliasingとの兼ね合いで、 UnsafeRawPointer はそのメモリ領域が現在どの型 T として扱われているかという事をコンパイラが静的に追跡するようです。これをバインドと呼びます。

UnsafeRawPointer としてアロケートした時は未バインドで、これをある型 T にバインドするメソッドが bindMemory です。それと同時に UnsafePointer<T> が返却されます。既に T にバインドされているメモリの場合は、 assumingMemoryBound メソッドを使えます。

未初期化なメモリをTに型付けしながら初期化する、 initializeMemory というメソッドもあります。

このあたりのバインドの状態遷移については、先述した文書の中に記述があります。Binding memory type

おわり

ポインタに対する専用構文を使わずに言語機能で必要な機能を提供し、型の情報はジェネリックに扱えてnull安全で、メモリの3状態についても明確に規約が整理された操作メソッドを提供していて、ムーブセマンティクスにも対応できるという事で、とても良くできていると思います。

RustやC++と対比して考えても面白いと思います。これらの言語は1級のポインタが生ポインタで、参照カウントなどがついたスマートポインタはジェネリック型として提供されます。しかしSwiftの場合はこれが逆転していて、1級のポインタがスマートポインタ、生ポインタはジェネリック型として提供されています。アプリを書くのにも使えるが、ローレイヤにも使える言語を設計する上で、この逆転はうまいバランスの取り方だと思います。