Nimで動的配列のポインタを扱う
要約
Nim で動的配列のポインタを扱いたいときは,要素の型が同じシーケンス値を作ることで便利に実現できます:
# 実行時に配列長が `n` とわかったとき
var x = newSeq[T](n)
動的配列のポインタは,次のように取得できます:
addr x[0]
こうすることで,配列の値をポインタ経由で受け渡しできるようになる上に,std/sequtils
などの seq[T]
の資源も活用できます[1].
使いたい場面
純粋な Nim プログラムを書く場合は,ポインタを扱うべきではありません.
GC 管理される参照 ref T
と比べて,ポインタ ptr T
はメモリ管理を自力で行わなければならず,コードの複雑化を招いたりバグの原因になったりするためです.Nim でポインタが必要になる主な場面は,FFI を使いたいときでしょう.
例えば,次のような Vulkan API を叩く場合は有効です:
import nimgl/vulkan
vkLoad1_0()
var layerCount = 0'u32
discard vkEnumerateInstanceLayerProperties(addr layerCount, nil)
# ここで初めて,`availableLayers` の要素数が判明する.
var availableLayers = newSeq[VkLayerProperties](layerCount)
discard vkEnumerateInstanceLayerProperties(addr layerCount, addr availableLayers[0])
# API から受け取る `VkLayerProperties` 型の値は,`availableLayers` の各要素として
# 取り出すことができる.
なぜこれで実現できるか
シーケンスの先頭要素へのポインタを動的配列のポインタとして用いてよい,と述べましたが,本当にこれで問題ないのでしょうか.この疑問に答えるためには,seq[T]
がどのように実装されているかを調べる必要があります.seq[T]
の実装は選択しているメモリ管理手法によって異なるのですが,refc などの旧来の GC と ARC/ORC とで 2 つに大別できます[2].それぞれで見ていきましょう.
ARC/ORC の場合
ARC/ORC でメモリを管理する場合,Nim のシーケンス型 seq[T]
の実体[3]は UncheckedArray[T]
にシーケンスの長さ情報をくっつけたものとなっています.
名前からもわかるように,シーケンスの要素は data
に配列の要素として格納されます.つまり,シーケンス値の先頭要素へのポインタは,配列の(先頭要素への)ポインタと実質同じなのです!
ARC/ORC 以外の場合
それ以外のメモリ管理手法を選択した場合でも,seq[T]
は配列長の情報がヘッダに付け加えられた配列として実装されています.ARC/ORC の場合と比べるとわかりにくいですが,実装は次のようになっています.
まず,シーケンス用のヘッダは TGenericSeq
として定義されています.ARC/ORC のときと同じく,ヘッダにはシーケンスの長さが格納されます:
シーケンスを作成するとき,メモリ領域は(ヘッダ分 + 要素分 len * typ.base.size
)の大きさで確保されます:
作成後は,TGenericSeq
型のヘッダ Sup
と配列 data[]
との構造体としてシーケンス型の値にアクセスできます:
シーケンスの要素は,同様に data
の要素として格納されます.この実装でもやはり,シーケンス値の先頭要素へのポインタが配列のポインタとして機能することがわかります.
まとめ
Nim で動的配列のポインタを扱う方法として,シーケンス x
の先頭要素へのポインタ addr x[0]
を取得することをご紹介しました.なぜこの方法でよいのかについても,シーケンスの実装から明らかにしました.
Discussion