さいきん Rust の基本戦略をやっと完全に理解しました。
- オブジェクトはスタックに割り当てます。(スタックポインタを減算するだけ)
- 所有権とライフタイムによってオブジェクトの生存期間が関数の生存期間を超えないように制御します。
- 関数を抜けたらスタックのオブジェクトを破棄します。(スタックポインタを元に戻すだけ)
2の所有権やライフタイムはコンパイル時にチェックされるので実行時は1と3のスタックポインタ操作だけになり、この仕組みだけを使ったオブジェクトは安全で高速に割り当てと解放が可能です。これが Rust の最も基本となるオブジェクト割り当て方針です。
この仕組みでオブジェクトの割り当てが不可能な状況が2つ存在します。オブジェクトのサイズがコンパイル時にわからないケースと、オブジェクトの生存期間が関数の生存期間を超える(かもしれない)ケースです。
オブジェクトのサイズがわからないとスタックポインタの操作が難しくなるのでスタックに確保できず、 Box
や Vec
などの専用のラッパークラスを経由してヒープに割り当てる必要があります。
*1
関数の生存期間を超えるライフタイムのオブジェクトは、ライフタイムを調整して静的またはヒープに割り当てるケースと、関数の戻り値にライフタイムを指定して調整するケースがあります。
静的に割り当てたオブジェクトは初期化メソッドが const fn
であるなどの制約がありますが、プログラムの実行中は常に生存している最も寿命が長いオブジェクトになります。また静的オブジェクトの参照を表す特殊なライフタイムが 'static
になります。
ヒープに割り当てる場合は一番制約が少ないですが、先述のように専用のラッパークラスを利用し、さらにアロケーターによる複雑なアルゴリズムでメモリの割り当てと解放を行うので最も遅くなるオブジェクトでもあります。
ヒープに割り当てたオブジェクトは所有権とライフタイムによって寿命管理され、誰からも参照されなくなったとコンパイラが判断した時に開放されますが、コンパイラの判断が難しいケースではさらに Rc
や Arc
などでラッピングして実行時に所有権を管理します。
*2
最後の関数の戻り値にライフタイムを指定するケースはオブジェクトがどうやって割り当てられているのかよくわからないのでもう少し調査が必要です。予想としては、呼び出す側の関数のスタックに場所だけ割りてておいて呼び出される側に渡してるんでしょうか?
以上のように、所有権とライフタイムをコンパイラが管理することで安全性を担保しつつ、可能な限りオブジェクトをスタックに割り当てることで高速に実行できるのが Rust の特徴ということがわかります。
そして、なぜコンパイル時に配列のサイズが確定できないとコンパイラが猛烈に怒り出すのかもこれでわかるかと思います。
Rust なんもわからん。