UnsafeBufferPointerで確保した領域の解放??

投稿者: Anonymous

ポインタの勉強中です。

class BaseClass{
    var value : Int
    init(_ a:Int){
        self.value = a
    }
    deinit{
        print("BaseClass:(self.value)")
    }

}

func ptr2<T: Any>(p: UnsafeMutablePointer<T>) -> UnsafeMutablePointer<T> {
    return p
}

do{
    var i = BaseClass(100)
    var p = ptr2(p:&i)
    print(type(of:p))

    var array = UnsafeMutableBufferPointer<BaseClass>(start:p,count:100)
    print(type(of:array))
    print(array.count)
    array[1] = BaseClass(200)
}

array[1] = BaseClass()が、解放されていません
なぜでしょうか?

解決

本題に入る前にSwiftでのポインタの学習をされるのであれば絶対に知っておいていただきたい点を先に述べておきます。

それは、

func ptr2<T: Any>(p: UnsafeMutablePointer<T>) -> UnsafeMutablePointer<T> {
    return p
}

のような形でinoutパラメータとして渡されたポインタを関数の終了後も使用するのは、Swiftではやってはいけないの一つであるということです。

Using本(Using Swift with Cocoa and Objective-C (Swift 3.0.1)Pointersの部分にこのように明記されています。

The pointer passed to the function is guaranteed to be valid only for
the duration of the function call. Don’t try to persist the pointer
and access it after the function has returned.

このようにして渡されたポインタが有効なのは関数の実行中だけです。そのようなポインタを関数外の変数に代入したり戻り値として返したりして関数から戻った後でアクセスしようとするのは絶対ダメです。

通常「してはいけない」を"you should not"で表現する技術文書で"Don’t try"と命令形を使っているのにはかなり強く禁止するというニュアンスが含まれるため絶対ダメと訳しておきました。(persistについては意訳してあります。)

残念ながらあなたが掲載されたようなコードをC言語の"address of"演算子(&)代わりに使えるものとして喧伝している困ったブログ記事等もちょくちょく見かけますが、Swiftに関する公式ドキュメントが乏しかった頃なら仕方ないかもしれませんが(私も1年かそこら前に書いたコードに似た書き方が残っているのを見て冷や汗かきながら修正することがあります)、Appleが公式ドキュメントにはっきり絶対ダメと書いてあることをやるべきではないでしょう。


と言うわけで本題です。

絶対ダメptr2関数を使ったまま話はしたくないのですが、そこを書き換えるとポインタやARC絡みの別の問題が発生するため、ここでは

  • ブロックローカルの変数iはスタック上に割り当てられ、pはそのアドレスを指している

と言うことにしておきます。(コンパイラの最適化レベルを上げれば、小さな関数のローカル変数なんかは全てレジスタに割り当てられます。とても妥当な仮定とは言えないのですが。)

簡単にまとめると:

  • UnsafeMutableBufferPointer(start:count:)は自前で領域を確保することはありません。
  • UnsafeMutableBufferPointer型の変数(あなたのコード例のarray)が指す領域が変数の有効期限の終わりに解放されることもありません。

UnsafeMutableBufferPointer(start:count:)start:パラメータにcount:で示した個数の領域を確保したポインタを渡すのはプログラマ側の仕事で、あなたのコードでは(上記の仮定が成立するとしても)変数i1個分の領域しか保証されないポインタをcount:100で渡していますから、array[1]からarray[99]までは一体どこを指しているのかわからない状態、後はもう何が起こってもおかしくないところです。
(運が良ければ、array[1] = BaseClass(200)の実行時にcrashして、そこがおかしいとわかるのですが、ポインタ絡みのバグは原因となる場所は通過して後で発覚することも珍しくはありません。だからこそのSwiftでは全てのポインタ型がUnsafeのネーミングをされているわけです。)


ところでUnsafeMutableBufferPointer<BaseClass>(start:p,count:100)が正しく100個分の領域を持つようにするには、上に書いたように領域を確保した上でそのポインタをstart:に渡さないといけないので、次のような書き方になります。

do {
    let p = UnsafeMutablePointer<BaseClass?>.allocate(capacity: 100)
    p.initialize(to: nil, count: 100)
    let bufPtr = UnsafeMutableBufferPointer(start:p, count: 100)
    bufPtr[0] = BaseClass(100)
    bufPtr[1] = BaseClass(200)
    //...
    print("deinitialize starts")
    p.deinitialize(count: 100) //<-BaseClassのインスタンスが解放されるのはここ
    print("deinitialize finished")
    p.deallocate(capacity: 100)
    print("deallocate finished")
}
/*↓
deinitialize starts
BaseClass:100
BaseClass:200
deinitialize finished
deallocate finished
Program ended with exit code: 0
 */

ここで注意して欲しいのはbufPtrを通じて代入した2つのインスタンスが解放されるのはdeallocate(capacity:)の実行時ではなく、deinitialize(count:)の実行時だということです。参照型などのプリミティブとは言えない型を指すポインタを自前で確保する場合は、このようにallocate, initialize, deinitialize, deallocateの4個セットを正しい順序で正しく実行してやらないとメモリリークや暴走・crashの原因になります。ご注意ください。

回答者: Anonymous

Leave a Reply

Your email address will not be published. Required fields are marked *