RustにおけるOwnershipの仕組み
前回に引き続き、Mozillaイチオシの言語であるRustを触ってみる。今日はメモリ管理の仕組みとしての「Ownership」に着目する。
ちなみに、今日書く内容はRust Guideの17章、OwnershipのGuide、PointerのGuideをつまみ食いしながら適当にまとめたものなので、原文読んでもらっても良い。
メモリ管理について
まずは背景として、メモリ管理の必要性について考えてみる。
一般に、計算機上で動くソフトウェアというのはデータをメモリ上に次々に確保して様々な計算等を行っていく訳だけど、メモリというのは有限の大きさしか持たない為にデータの追加ばかりしていく訳にも行かなくて、使わなくなった領域を次々に解放して再利用しなければならなくなる。
こういった事を考えると、メモリ解放の機構を言語に組み込む必要性があると分かる。
メモリに確保されるデータの種類
メモリに確保されるデータには、大別すると次の3種類がある。
- ソフトウェア実行中はずっと残る静的なデータ
- プロシージャ呼び出し時に確保され、プロシージャを抜けると解放されるスタック上のデータ
- ソフトウェア実行時に動的に確保されるデータ(ここで確保される領域をヒープメモリと呼ぶ)
メモリの解放という文脈でこれらのデータを考えると、1の静的なデータはずっと残るものなので解放されないし、2のスタック上のデータは勝手に解放されるので気にしなくて良いのだけど、3の動的に確保されるデータをどう扱うかが問題になってくる。
c言語では、mallocやfreeによってユーザーが責任を持ってメモリの確保と解放を行うことが求められたけど、メモリリークや不正なメモリへのアクセス等が起きやすく大きな問題を抱えていた。
一方、近年の多くの言語では Garbage Collection(GC) の機構を組み込みで持つことでユーザーをメモリ管理の複雑さ・煩雑さから解放したが、 GC 実行時にストップザワールドが起こる等のデメリットもあった。
そこで、Rustは「Ownership」という考え方を導入することで、メモリリークの危険性を抑えながらパフォーマンス上の問題を解決することを目指した。
Ownership
動的にメモリを確保する際、例えばc言語では
{ int *x = malloc(sizeof(int)); // we can now do stuff with our handle x *x = 5; free(x); }
の様な書き方をする。同様の事が、Rustにおいては
{ let x = box 5i; }
という記述で表現できる。ポイントは、この記述できちんとメモリの確保と解放が表現出来ている事だ。
box というキーワードによって 5i という値がヒープ上に動的に確保され、その参照がxへbindされている。このxがownershipを持っていて、xがscopeから外れる(xを含むブロックを抜ける)時にメモリの解放が行われる。
この様に変数にownershipを持たせる事で、メモリの解放が必ず行われる事を保証できる様になる。
ownershipは移動させることもできて、例えばBox型の値(boxキーワードでヒープに確保した値への参照)を引数として関数を呼び出した場合には、ownershipはその関数内の変数numに移る。
fn main() { let x = box 5i; add_one(x); } fn add_one(mut num: Box<int>) { *num += 1; }
ただし気をつけなければならないのは、ownershipを移すと、ownershipを失った変数からは元の値へアクセス出来なくなる事である。上記の例であれば、add_one(x)呼び出しの後にxを使おうとするとエラーが出る。
fn main() { let x = box 5i; add_one(x); println!("{}", x); // error!!!!! } fn add_one(mut num: Box<int>) { *num += 1; }
この問題を避けるために、例えば関数から返り値を使うといった方法が考えられる。
fn main() { let x = box 5i; let y = add_one(x); println!("{}", y); } fn add_one(num: Box<int>) -> int { *num + 1; } //注: これはあくまで例で、あまり良い方法では無い。
ただし、今回の例ではnumの所有権は一時的なものであれば良く、返り値という形ですぐにまたmainのyに戻されている。こういった一時的な所有権というシチュエーションは良くあるものであり、上記の例よりもBorrowingという仕組みを使う方が推奨されている。
Borrowing
Borrowingとは、ownershipは移さずに値への参照を可能にする機構である。&キーワードで値への参照を作ると、それは値をborrowingする為のpointerとなる。例えば、ownershipを移すことで実現していた上記の例は、以下のコードに書き直せる。
fn main() { let x = box 5i; println!("{}", add_one(&*x)); } fn add_one(num: &int) -> int { *num + 1 }
呼び出し方がちょっとややこしくなってるけど、 *x で5iという値を意味していて、さらにその値をborrowingするために&キーワードで参照を作っている。
xのownershipは移動しない為、何度でもxの値を使用できる。
fn main() { let x = box 5i; println!("{}", add_one(&*x)); println!("{}", add_one(&*x)); // 何度でもxを使える!! println!("{}", add_one(&*x)); // 何度でもxを使える!! }
良い感じ。
mutableなborrowing
mutableなborrowingというのも一応出来て、&mutキーワードを使ってmutableな参照を作れば良い。ただし、もともとの値がmutableでないといけない。
fn main() { let mut x = box 5i; add_one(&mut *x); println!("{}", x); // xの値はborrowingされただけで移動してないから使える!! } fn add_one(num: &mut int) { *num += 1 }
mutや&mutや*の様なキーワードが入り乱れる事になるのでちょっとつらい感じになる。
Owner、およびBorrowerの権限について
Rustにおいては、ownerやborowerにはそれぞれ独自の権限がある。ownerは、次の3つの権限を持つ。
owner の権限
- リソースがいつ解放されるかを制御出来る。
- リソースを immutable な形で多くの borrower に貸し与えることが出来る。
- リソースを mutable な形で1つの borrower に貸し与えることが出来る。
ただし、これは次の2つの制約の存在も意味する。
owner の制約
- 既に誰かに貸し与えたリソースを変更したり、mutable な形で別の誰かに貸し与えたりする事は出来ない。
- 既に誰かに mutable な形で貸し与えたリソースは、アクセスすることが出来ない。
基本的に、変更によって変な競合が起きる可能性のある事は禁止されてるっぽい。
また、borrowerには次の権限がある。
- borrowがimmutableなら、リソースの読み取りが出来る。
- borrowがmutableなら、リソースの読み書きが出来る。
- 他の誰かにリソースを貸し与える事が出来る。
ただし、borrowerにも重要な制約があり、
「他の誰かにリソースを貸し与えたら、自分のborrow backの前に貸し与えた分を borrow backしてもらわなければならない」
らしい。この制約が守られないと、例えばownerのスコープが終了してメモリが解放され、不正な値となった領域にborrowerがアクセスする、といった危険な事が起きてしまう。そういった意味で、大事な制約となっている。
所感
Ownershipについて調べてみて、Cなんかと比べて一番便利なのはヒープ上に確保した値をプロシージャで安心して返せることなのかもなーと思った(返り値のownerが明確であれば、メモリの解放を保証出来るから)。
正直、Cでバリバリコーディング出来るとかでは全然無いんだけど、解放が保証されるか分からないポインタを使うとか想像するだけで怖すぎるなーとは思う。
で、C++におけるメモリ管理のお作法がちょっと気になったのでGoogle C++スタイルガイド 日本語訳とかいうのを見てみたら、現代的なC++コーディングではスマートポインタという解放が保証された機構を使うものらしい事を知った。
基本的には所有権の移動が無い scoped_ptr を使って、どうしても共有の必要性が出てきたら(参照カウントを組み込んだ) shared_ptr を使うと。なるほど。
割とRustと似たような事をやるっぽい。