RustはトレースGCを持たずRAIIと所有権に基づくメモリ管理を行います。これには様々な利点がある一方、相互参照をもつデータの扱いが他のプログラミング言語より難しいという困難があります。本記事では、あまり一般的ではないが特定の限られた用途では有用と思われる方法を紹介します。
標準的な方法
まずは相互参照が起きないように設計を再考するのがいいでしょう。特に「子データから親データを参照する」といったユースケースでは、必ずしも子データ自体が親データへの参照を持たなくてもいいことがあります。以下ではこれに当てはまらない例、典型的にはグラフの表現を念頭に置いて記述します。
Rustで相互参照を扱う最も標準的な方法は、typed_arena
などのアリーナアロケーターと RefCell
などの内部可変性コンテナを組み合わせる方法です。これについては私のブログ記事などを参考にしてください。
またRustの Arc
を読む (1)ではArcとWeakを使った方法も説明しました。
ノーガード戦法
Rustにおける相互参照が難しいのは、結局のところメモリやリソースの解放の順番の問題に由来しています。たとえばアリーナアロケータは個別に確保し、一括で解放することによってこの問題を解決しています。
では、そもそもメモリやリソースを解放しなくていい状況であればどうでしょうか?「1プロセスでは1つの問題しか解かなくてよい」という割り切りが可能なのであれば、自身でリソースの解放処理を行わなくても、単にプロセスを終了まで持っていったほうが簡単です。その場合は単にメモリリークを許容することで簡単に相互参照を実現できます。
// ノーガード戦法による相互参照
// 意図的にメモリリークを行っているので、通常の用途では使ってはいけない
use std::cell::RefCell;
struct Node {
id: i32,
edges: RefCell<Vec<&'static Node>>,
}
impl Node {
fn new(id: i32) -> &'static Self {
// Box::leak を使うことで意図的にメモリリークを起こしている
Box::leak(Box::new(Node {
id,
edges: RefCell::new(vec![]),
}))
}
}
fn main() {
let node1 = Node::new(1);
let node2 = Node::new(2);
let node3 = Node::new(3);
node1.edges.borrow_mut().push(node2);
node2.edges.borrow_mut().push(node1);
node2.edges.borrow_mut().push(node3);
node3.edges.borrow_mut().push(node2);
eprintln!("node1 has {} edges", node1.edges.borrow().len());
eprintln!("node2 has {} edges", node2.edges.borrow().len());
eprintln!("node3 has {} edges", node3.edges.borrow().len());
}
FAQ
- メモリリークして大丈夫か?
- 一般的には大丈夫じゃないです。が特定の場面では有用かもしれない、というのが本記事の意図です。
- 「メモリリークを厳密に防ぐのは難しい」ということがLeakpocalypseと呼ばれる議論によってわかって以来、Rustではメモリリークは許容することになっています。メモリリークが起きても、それ以上の深刻な事象 (境界外参照やデータ競合などの未定義動作) は発生しないように設計されています。この意味で、メモリリークは「安全」という扱いになっています。
- とはいえ、メモリリークは良いことではないので、意図しないメモリリークはできるだけ防ぐように設計されています。たとえば上記のプログラムでは
Box::leak
というメソッドを呼ぶことで明示的にメモリリークを起こしていますが、このようにプログラマーがメモリリークを意図しているとはっきりわかる方法をとらずにメモリリークを起こすのは簡単ではありません。
-
RefCell
は回避できないのか?