mutなスマポ (BoxとかRcとかCellとか)

  • 42
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

スマートポインタのことをスマポって略してみました。

Rustのポインタ・所有権管理は本当にくっっそ面倒もとい厳格です。 使える武器を全部知っておかないと、ちょっとしたプログラムを書くのにも苦労間違いなしです。 だいたいのチュートリアルではBoxRcの簡単な使い方には触れますが、意外とCellmut変数の使い方に触れてるものが少ない(ような気がした)ので書きました。

(注:型を読者にわかりやすくするために、変数宣言ではlet a : i32のように型を記入していますが、本来ほとんどが省略可能です)

mutな参照

let mut a : i32 = 1;
{
    let ref_to_a : &mut i32 = &mut a;
    *ref_to_a = 32;
}
println!("a = {}", a);  // a = 32

これはまぁ、普通に言語チュートリアルでも出てくるようなプログラムです。
このプログラム中にmutキーワードが3回出てきますが、このうちどれを消してもコンパイルが通らなくなります。 それぞれのmutの意味については、まぁなんとなくわかりますよね? でもここではいちいち書き下します。

  1. 最初のmutは、変数aの値が変更可能であることを示します。
  2. 2番目のmutは、変数ref_to_aは「参照先の値を変更可能な」参照型であることを示します。
  3. 3番目のmutは、変数aから「参照先の値を変更可能な」参照をとってくることを示します。

実はこのプログラム中には、もう1ヶ所mutを挿入できるところがあります。 ここです。

    //  vvv ここ!!
    let mut ref_to_a : &mut i32 = &mut a;

ここにmutを挿入すると、変数ref_to_aは「参照先を変更可能な」変数であることを示します。 2.と3.の、「参照先の値を変更可能な」とは意味が違います。 このmutがあると、例えばこんなことができます。

let mut a : i32 = 1;
let mut b : i32 = 2;
{
    let mut ref_to_a : &mut i32 = &mut a;
    ref_to_a = &mut b;  // ここでaの代わりにbを参照させる
    *ref_to_a = 32;
}
println!("a = {}, b = {}", a, b); // a = 1, b = 32

どうでしょう。 C/C++のconst ptrの概念に慣れた人ならぱっとお分かりいただけるかと思いますが、constのない言語しかやったことのない人はちょっと混乱するかもしれません。 しかし、この区別はRustをやる上で避けて通れない区別です。 がんばってなんとなく理解してください。

mutBoxmutRc

これの発展です。 参照のかわりに、BoxRcを使うケースではどうでしょう(BoxRcの意味についてはここでは書きません、その辺のドキュメントを読んでください)。 ここではRcを例に挙げます。
やはりここでも、「参照先の値を変更可能な」ものと、「参照先を変更可能な」ものを区別して考える必要があります。

参照先を変更可能な」Rcの方は比較的わかりやすいです。 mutを変数名の前に追加してやるだけです。

参照先を変更可能なRc
use std::rc::Rc;
let mut a : Rc<i32> = Rc::new(1);
a = Rc::new(32);  // ここで参照先を変更している
println!("a = {}", *a); // a = 32

しかし、「参照先の値を変更可能な」Rcというのは一筋縄ではいきません。 参照を使ったコードから類推して、Rc<>の中にmutを書きたくなりますが、これはコンパイルできません。

コンパイルできない
use std::rc::Rc;
let a : Rc<mut i32> = Rc::new(1);
*a = 32;
println!("a = {}", *a); // a = 32
エラーメッセージ
error: expected identifier, found keyword `mut`
let a : Rc<mut i32> = Rc::new(1);

特にC/C++をやったことのある人が起こしやすい勘違いなのですが、Rustのmutは、C++のconstと違い、型の修飾子ではありません。 mutが書けるのは、基本的に変数宣言のletの直後か、参照の&の直後だけと覚えておきましょう(一応他にもありますが)。

ではどうすれば、Rustで合法的にRcの参照先の値を書き換えられるのでしょうか。

Rc::make_mutRc::get_mut

まずは不正解の答えから行きます。 Rc型のドキュメントを読んでいると、どうやらこの2つの関数を使えば、中身の&mut T型にアクセスできそうな気がしてきます。 しかし実は、この2つの関数には非常に強い制約があり、あなたの期待しているものではない可能性が高いです。

どういうことかというと、どちらの関数も「そのRcの参照カウントが1のときだけ」しか、参照先の&mutを返してくれないのです。

use std::rc::Rc;
let mut a : Rc<i32> = Rc::new(1);  // Rcを新しく作ったので、参照カウントは1
*Rc::make_mut(&mut a) += 1;  // make_mutを使って、aの参照先に1を足した
println!("a = {}", *a);  // a = 2

// Rcをコピーしたので、参照カウントは2
let b : Rc<i32> = a.clone();
println!("a = {}, b = {}", *a, *b);  // a = 2, b = 2

// aの参照先の値に1を足したい……もちろんbの参照先の値も1増えるはず???
*Rc::make_mut(&mut a) += 1;
println!("a = {}, b = {}", *a, *b);  // a = 3, b = 2  bは増えてない!

2回目にmake_mutを呼び出したときには、1回目のときとは違いaの参照カウントは2になっています。 すると、make_muti32::clone()を呼び、新しいRcとその参照先を作成してしまうのです。 なんでこんな不便な仕様なの? というのはめんどくさいので触れません。 代わりにどうすればいいのかを考えましょう。

Cellを使う

そこで登場するのがCellです。 CellBoxと同じように、他の型1個をラップするオブジェクトで、getsetメソッドを提供します。

let a : Cell<i32> = Cell::new(1);
a.set(32);
println!("a = {}", a.get());  // a = 32

これを見るとなんでわざわざCellなんて必要なんだ、と思われるかもしれませんが、よくよく見てください。 amutな変数ではないのに、setメソッドが呼び出せていますね。 Cell型は、mutでなくてもsetメソッドを呼び出すことができます。 つまり、さっき書こうとしていた、Rcの参照先の値に1を足してみるコードというのは、このように書くことができるのです。

let a : Rc<Cell<i32>> = Rc::new(Cell::new(1));
(*a).set((*a).get() + 1);
println!("a = {}", (*a).get());  // a = 2

RcCellの二重構造になってややこしいと思いますか? これがRustだ。 一応ちょっとだけ簡略化できて、Rc型がDeref traitを実装しているおかげで、(*a).get()のような部分はa.get()のように書けます。

これなら、Rcの中にmutと書くことなく、参照先の値を変更可能なRcを作ることができます。

さて、これだけ見ると、Cellはただ単に普通の変数のmut制限をゆるくしただけの「軟派な」型のように見えますが、そうではありません。 普通の変数にはできるけどCellにはできないことというのが結構あります。

  1. Cellの中身の型はCopyをimplしていなければならない (i.e. memcpyでコピーできなければならない)
  2. Cellは参照やスマートポインタなどを含め、スレッド間でやりとりすることができない
  3. Cellは中身の参照を取ってくることができない

特に2.の制約がわかりやすく強力で、Cell型は1つのスレッドからしか読み書きされないことが保証されているので、いくつ変数のaliasが作られて何ヶ所から書き込まれようとも、データが同時書き込みや書き込み中の読み込みによって壊れてしまう心配がありません。 なので、mutとマークされていなくてもsetを呼び出せるようになっているのです。

RefCellを使う

しかし、Cellの制限はちょっときつすぎます。 一番きついのは1.で、Copyな型というのは単純な整数型や、整数型のみからなるstructなどに限られ、HashMapなどのように内部にポインタ的なものを含むような型には使えません。

また3.の条件も地味にきついです。 例えば超長い配列をCellに入れたとすると、getsetするたびにいちいちコピーが発生します。 超長い配列のうち1番目の要素だけを変更したい場合でも、いちいち全体をコピーしてきて1番目を書き換え、また全体を書き戻す……という作業が必要になります (たぶんここに最適化は効きません)。

なので、たいていの場合はそこの制限が緩和されたRefCellを使います。 RefCellはその名の通りCellの中に参照型を持ってるようなもので、Cellと比べて以下のような利点があります:

  1. RefCellの中身の型はだいたい何でもいい (Sized)
  2. RefCellは中身を参照やmut参照で取ってくることができる (RefRefMut経由)
  3. RefCellの中身を参照で取れるということは、その参照を他のスレッドに安全に渡すこともできる

なんだCellの欠点が全部取り除かれてて最強じゃないか、と思いますが、ちょっと違うのはCellgetsetメソッドが取り除かれていて、代わりに参照っぽい型のRef型を返すborrowと、mut参照っぽい型のRefMutを返すborrow_mutメソッドで置き換えられていることです。 この2つのメソッドと戻り値には、普通の変数の&&mutに似た特別なルールがあり、

  1. Ref型のオブジェクトは同時に何個でも存在できる
  2. RefMut型のオブジェクトが1つでも存在すると、他のRef型およびRefMut型のオブジェクトは存在できない

というルールがあります。

このRefCellが普通の変数と決定的に違う点は、このルール違反が起こった時に、普通の変数ならコンパイルエラーが出ますが、RefCellでは実行時にpanic!して落ちるという点です。 RefCellを使う際には、プログラマの責任でborrow_mutの呼び出し場所をきっちり管理してやる必要が生じます。 これを多用しすぎると、くっそ面倒くさいRustでコードを書く意義が薄れてしまうので、必要のないところではできるだけ使わないようにしましょう。

ただし、これが必ず必要になる場面というのがいくつか存在します。 詳しくはstd::cellの公式ドキュメントを読んでください。

まとめ

  • Rcの参照先の値を変更したいときは、だいたいRc<RefCell<T>>を使う。 かなりの頻出パターンなので覚えておきたい
  • 1%ぐらいの確率でRc<Cell<T>>Rc::get_mut, Rc::make_mutが使えるかもしれないので、一応ドキュメントを読んでおく
  • でも本当はあんまりそういうコードを書きたくない