Zig言語のArrayListにおけるポインタ安定性の仕組みを徹底解説
Zig: Pointer Stability for ArrayLists
Zig: Pointer Stability for ArrayLists
ZigのArrayListを使用する際、要素の追加や削除によって内部メモリが再確保されると、それまで指していたポインタが無効になってしまう「ポインタ安定性」の問題が頻繁に発生します。本記事では、この挙動の仕組みと、安全にメモリを扱うためのベストプラクティスについて深掘りします。
良い機能だとは思うけど、コレクション内のアイテムへの安定したポインタが必要なら、そもそもArrayListは不適切なデータ構造じゃないかと思えてしまう。誰か、インデックスを保存する代わりになぜこれを使うのか、例を挙げてくれないかな?それか、Unrolled Linked List(Zigの0.16で削除される前はSegmentedListと呼ばれていたけど、なぜ削除されたのかは不明)を使うという手もあるよね。
これじゃ弱すぎる気がする。
Rustのような言語なら、コンパイラがポインタを「ロック」してくれるから忘れることはない。
C++(そしておそらくZigも)のような言語であれば、理論上は、動的配列のストレージを参照するイテレータやスライスに、ストレージをピン留めするようなロックを持たせることも可能なはず。
でもこのAPIだと、プログラマがポインタのロックを忘れないようにしなきゃいけないし、コードの適切な範囲でロックを維持し続ける必要もある。ブログ記事の例でさえ、安定性が必要な関数の完全に外側でロックを取得していて、ロックのスコープを正しく管理する仕組みが全くないように見える。型システムでさえ役に立たない。問題のparse関数側で、ポインタロックされたArrayListのパラメータが必要だと宣言することすらできないからね。
余談だけど、Rustで本当に恋しいZigの(構文上の)機能が、2番目のコードブロックにあるプリフィックス付きの複数行文字列リテラルなんだ。
const text =
\\これは長いコメント
\\でも行を自由に分割できるし
\\インデントも保てる。
;
最近はRustのdocstr [1]というマクロライブラリを使い始めていて、同じようなことができるよ。
const TEXT: &'static str = docstr!(
/// これでRustでも同じことが可能に。
/// 長いテキストにはこのスタイルの方が
/// 気に入っているんだ。
);
マクロとも組み合わせられる(ドキュメントより例):
let greeting: String = docstr!(format!
/// こんにちは、私の名前は {name} です。
/// {} 歳です!
age
);
彼らは言い忘れているけど(意図的ではないと思う、ユーザーなら推測できるはずと考えているのかも)、このチェックはDebugモードとReleaseSafeモードでしか実行されず、ReleaseFastモードでは機能しない。スーパーホットなコードパスでのメモリ書き込み/読み込み/分岐だから、ある意味合理的だとは思うけど、個人的には保証の大部分が損なわれている気がする(Zigには今回の例のようなミスを捕捉できるデバッグアロケータがあるはずだよね?)。
これは個人的な愚痴だし、弱い意見だとは自覚しているけど言わせてほしい。
セグメンテーションフォールトをスタックトレース付きのパニックに変えるのは、開発体験としては改善かもしれない。でも、それによってソフトウェアが良くなるわけじゃない。メモリ安全性のミスを緩和するための自動戦略としては、GCを使ってプログラムを健全に保つか、静的解析でミスを構造的に防ぐ手法の方が明らかに優れている。
一部のシステムプログラミング界隈には、「複雑さ」(言い換えれば、根本的な問題を修正すること)を避けて、プログラマがミスをした時にわかりやすいエラーメッセージを出す方向に流れる傾向があるけど、自分はそれを「より良いソフトウェア」だとは思えないんだ。
SDLのサーフェスのようにデータ構造をロック/アンロックするのではなく、コンテナ内のポインタ変更をアサートしているんだと理解するまでに少し時間がかかった。
最近、std::stringをバックエンドにして、コンポーネントをイテレートできるパス用のカスタムC++コンテナを実装したんだ。単にインデックスと文字列への参照を保持するようにしたから、std::stringが変更されて再確保されてもイテレータが無効にならない。コストもほとんどかからず、エラーも起きにくいからおすすめだよ。
言語の他の部分との一貫性を考えれば、これは理にかなっていると思う。プログラミングミスを捕まえるための「トラップ」をコード内に仕掛ける手段が一つ増えたという感じだね。関数内で入力や出力を検証するためにアサートを使うのと似ている。
自分はArrayListをよく使うから、コード全体にこれを組み込んで堅牢にしていこうとワクワクしているよ。
万人受けするものじゃないとは思うけど、もしこれが嫌なら、たぶん言語の他の明示的な機能も楽しめないんじゃないかな。
C++のstd::vectorの要素参照にはずっと悩まされてきたから、Zigのこの明示的な安定性はデータ構造において歓迎すべき救いだよ。
C++の開発者は常にvectorのイテレータ無効化と戦っているから、Zigがこれを第一級の関心事として扱っているのは素晴らしいね。