スマートポインタとしてのBox
Rustでは、プログラム実行中にデータをメモリに格納するとき、状況に応じてスタックかヒープのどちらかに格納します。
スタックは出し入れが高速な一方で、データの型(すなわち、確保しなければならないデータ領域のサイズ)が定まっている必要があります。ヒープは可変サイズのデータも格納できる一方、アクセス速度はスタックに劣ります。
デフォルトでは可能な限りデータはスタックに格納されます。もしデータを明示的にヒープに格納したい場合は、スマートポインタのBox
を利用できます。データの本体はヒープに置きつつも、そのデータへの参照をBox
として宣言すれば、その参照自体はスタックに積むことができます(なぜなら、参照自体は固定長だから)。
Box
の使い方
Box::new(値)
で作成できます。
fn main() {
let x = 5; // スタックに格納
let y = Box::new(5); // ヒープに格納
println!("x = {}", x);
println!("y = {}", y);
}
Box
を使うユースケース:再帰的なデータ構造
Box
を使ってあえて明示的にヒープ領域を使うように指示するユースケースとして、再帰的なデータ型(構造体)を定義したいときがあります。
ある構造体のプロパティにその構造体自身が含まれている場合(構造体のネストがあり得る場合)、構造体のネストがどれだけ続くか分からず、構造体のデータサイズが決定できません。そこで、構造体そのものを含むのではなく、構造体へのポインタを含むように定義することで、構造体自身のサイズは固定になります。
enum List {
Node(i32, Box<List>),
Nil,
}
fn main() {
let list = List::Node(1, Box::new(List::Node(2, Box::new(List::Nil))));
}
上記の例では、List
は再帰構造になっていますが、Node(i32, Box<List>)
のようにBox<T>
を使ってスマートポインタを含むようにしています。もしこれをNode(i32, List)
のようにしてしまうと、コンパイラエラーになります。
*
で行うDeref
ポインタに*
をつけて参照先の値を取得する操作をderefといいます。Box
はスマートポインタでポインタの一種であるため、derefすることができます。
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, *y); // Derefを使用して値にアクセス
}
*
をつけてderefできることは、厳密にいえばDeref
トレイトを実装していることと等しいです。言い方を変えれば、Deref
トレイトを実装することで、独自の型であっても*
を使ってderefすることができます。
独自にDeref
を実装する
Deref
を実装するには、以下の定義が必要です。
- 関連型
Target
-
Target
を返すderef
メソッド
関連型とは、トレイトが要求する型で、トレイト実装のために必要なメソッド(今回でいえばderef
)のシグネチャに必要な型になります。deref
メソッドによってどのような型が返却されるかは、実装先の構造体がどのような型を返すつもりなのかによって決まります。そのため、実装先の構造体にTarget
という型を決めさせておき、それを返すようにderef
を定義しています。
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, *y); // Derefトレイトにより値にアクセス
}
分かりにくいですが、MyBox
はタプル構造体になっています((T)
)。そのため、値は&self.0
から取得できます。
参照外し型強制
Deref
トレイトを実装するメリットの一つに、参照外し型強制があります。Box::new
を使って作成したスマートポインタの型はBox<T>
になります。
例えばある関数が&i32
を要求しており、その関数の引数にBox<i32>
を渡したいとします。Box<i32>
をderefすることでi32
を取り出す必要があるように思えますが、実際はその操作をせずともコンパイルに成功します。
fn print_value(x: &i32) {
println!("{}", x);
}
fn main() {
let value = Box::new(42); // Box<i32>
print_value(&value); // Rustが自動的に参照外しして&Box<i32> -> &i32に変換
}
これは参照外し型強制という仕組みで、Deref
トレイトが実装されている型を関数の引数に使うと、引数として要求されている型になるまで自動でderefを繰り返してくれる機能です。