【Rust入門】Boxの基本について分かりやすく解説

Rust でヒープ上にデータを確保するための Box の基本を初心者にも分かりやすく解説します。
Box 型の概要
Box 型とは
Rust では、スタック領域にデータが格納される基本型と String や Vec のように、内部的にヒープ領域に確保される型があります。Box 型は、特定の用途に限定された型とは異なり、任意の型をヒープに確保することができる型です。
C/C++ 言語などで、データの格納しているメモリ領域を指し示すものをポインタと言いますが、Rust では、データの所有権と追加機能を持つポインタが用意されており、これをスマートポインタと言います。Box は、最も基本的で汎用的なスマートポインタです。
Box 型の主な特徴は以下の通りです。
- ヒープ上にデータを確保する。
- 所有権を持ち、スコープを抜けると自動的にメモリが解放される
- スタック上のサイズは、ポインタ分のサイズに固定できる
Rust では、通常の基本型はスタックにデータを確保しますが、Box 型を使うことで明示的にヒープへデータを配置することもできます。
この記事では、Box 型の基本的な生成方法から、具体的な使用例を紹介します。
なぜ Box が必要なのか
まずは、そもそも Box がなぜ必要になるのかを考えてみましょう。
Rust では変数のサイズをコンパイル時に確定している必要があります。しかし、以下のようなケースではコンパイル時にサイズを確定することができません。
- 型が自分自身を再帰的に参照する構造(連結リストなど)
- 実行時までどの具体的な型が入るか分からない場合(トレイトオブジェクトなど)
こうしたケースでは、実データをヒープ上に配置し、スタック上には固定サイズのポインタデータだけ持つようにすることでコンパイルができない問題を回避できます。これを実現できるのが Box 型です。
なお、Box 型は所有権の仕組みをそのまま利用できるため、スコープを抜けるとヒープ上のデータは自動的に開放され、Rust の特徴であるメモリ安全性が確保されます。
Box の生成方法
Box 型を生成するには、Box::new メソッドを使用します。
fn main() {
// new で i32 型の値をヒープ領域に確保する
let b = Box::new(5);
println!("b : {b}");
}【実行結果】 b : 5
本来、i32 型はメモリのスタック領域に確保される小さなサイズの型であるため、あえてヒープに確保する必要性はありませんが、Box の例として分かりやすいので紹介しています。i32 型以外の型でも同様にヒープ領域に確保することができます。
Box 型では、スタック上にヒープ領域を指し示すポインタの領域だけ確保され、実体である値については、ヒープ領域に格納されます。例において、b がスコープを抜けるとヒープ上のデータも自動的に開放されます。
Box の中身にアクセスする
Box 型の中身にアクセスするには「*」で参照外し(Dereference)を行います。
fn main() {
let b = Box::new(5);
// * で参照外しを行い、Box の中身の値を取り出して計算に使用する
let result = *b + 10;
// Box は、Display トレイトを実装しているため、println マクロで参照外しが不要
println!("b : {b}");
println!("result : {result}");
}【実行結果】 b : 5 result : 15
例では、*b により、Box 型のポインタが参照するヒープ領域の値 (5) を取り出して、計算に使用し、結果を result に格納しています。
「*」による参照外しが実現できるのは、Box 型が Deref トレイトを実装しているためです。また、例の println! では、参照外しをしていませんが、Box の Display トレイトで参照外しが行われるため、そのまま b を渡すだけで値の表示ができます。
Box の活用パターン
基本的な Box 型の使用方法を紹介しましたが、ここでは Box が真価を発揮する代表的な活用パターンについて紹介します。
再帰的なデータ構造を定義する
再帰的なデータ構造をもつ連結リストの例を用いて、Box の使い方を紹介します。この例は、Rust Book のこちらでも紹介されている Box の代表的な使用例です。
i32 型の値とそれに連結されるリストを持ち、末尾は Nil で表現するような List 型を定義することを考えてみます。以下のプログラムでは「error[E0072]: recursive type List has infinite size」というコンパイルエラーとなります。
// コンパイル時にエラーとなる例
// コンパイラは、List 型のサイズを決定できないため、コンパイルエラーとなる
enum List {
Cons(i32, List),
Nil,
}
use List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}Rust のコンパイラは、型定義を見てその型のサイズがどれだけかを確定しようとします。上記例の List は、i32 の後に続くリストのサイズがどれだけ続くか分からないため、無限のサイズとしてエラーとなります。
例における list 変数は、1, 2, 3, Nil のようにサイズが計算できそうに見えますが、ここでコンパイルが計算しているのは特定の値の構造ではなく、List という型そのもののサイズを型定義から計算しようとしているという点を誤解しないように注意してください。
上記のプログラムは以下のように Box 型を使用することで解決できます。
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
// List の中身を表示する関数
fn print_list(list: &List) {
match list {
// List の中身を表示する
Cons(value, next) => {
println!("{value}");
print_list(next);
}
// 末端の Nil の場合は、Nil と表示する
Nil => println!("Nil"),
}
}
fn main() {
// 1 -> 2 -> 3 -> Nil という連結リストを表す
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
print_list(&list);
}【実行結果】 1 2 3 Nil
この例のように Box<List> とすることで、この部分で必要なサイズはポインタのサイズとなり、List 型のサイズは計算可能となります。実体データは、ヒープ上に置かれるため、どのようなサイズになるかを意識する必要がありません。これにより、コンパイルは無事成功し、処理を実行することができます。
トレイトオブジェクトを扱う
次の例としてトレイトオブジェクトを扱う例を紹介します。
任意のエラートレイトを扱う
? 演算子を使用して、関数内のエラーの処理を上位へ移譲する例を考えてみましょう。
use std::error::Error;
fn parse_and_double(input: &str) -> Result<i32, Box<dyn Error>> {
// 文字列を i32 に変換する(失敗すると ParseIntError が返る)
let value: i32 = input.parse()?;
if value == 0 {
// 独自のエラーメッセージを Box<dyn Error> として返す
return Err("値に 0 は指定できません".into());
}
Ok(value * 2)
}
fn main() {
// 値を変更して試してみてください
let inputs = ["21", "abc", "0"];
for input in inputs {
match parse_and_double(input) {
Ok(result) => println!("result : {result}"),
Err(e) => println!("error : {e}"),
}
}
}【実行結果】 result : 42 error : invalid digit found in string error : 値に 0 は指定できません
parse_and_double 関数は、成功時は Ok(value * 2) のように 2 倍にした値を返却します。また、"abc" という文字列のように i32 への変換ができないものに対しては、ParseIntError を、"0" の場合は、独自のエラーを返します。このように、関数が返す可能性のある具体的なエラー型は、実行時の入力によって変わります。
このような場合は、返却値を Result<i32, Box<dyn Error>> とすることでどちらのエラーも統一的に扱うことができます。
dyn Error というのは、Error トレイトを実装する型を表す動的な型です。Error を実装する型ごとにサイズが異なるため、dyn Error はコンパイル時点でサイズを確定できません。この型に、Box を適用することで、Error トレイトを実装する任意の型をヒープ上に格納し、統一的に扱うことができます。
コレクションで同じトレイトを持つ型をまとめて扱う
Box<dyn Trait> は、異なる型を同じコレクションにまとめて格納したい場合にも利用されます。以下の例は、同じ Shape トレイトを実装した Circle と Rectangle を 1 つの Vec にまとめて扱う例です。
trait Shape {
fn area(&self) -> f64;
}
struct Circle {
radius: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
fn main() {
// 異なる型(Circle、Rectangle)でも、同じ Shape トレイトを実装していれば
// Box<dyn Shape> として同じ Vec にまとめて格納できる
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 2.0 }),
Box::new(Rectangle {
width: 3.0,
height: 4.0,
}),
];
// 実行時に、それぞれの型の area メソッドが呼び分けられる(動的ディスパッチ)
for shape in &shapes {
let area = shape.area();
println!("area : {area:.2}");
}
}【実行結果】 area : 12.57 area : 12.00
例における Circle と Rectangle はそれぞれ異なるサイズ・実装を持つ型ですが、いずれも Shape トレイトを実装しています。このような場合には、Vec<Box<dyn Shape>> のようにすることで、Shape トレイトを実装している型を同じ Vec に格納することができます。
Vec では、格納する要素は同じサイズでなければならないという制約がありますが、Box<dyn Shape> は、ヒープ上の実データへのポインタであるため固定サイズの値となり、この制約を満たすことができます。
まとめ
Rust でヒープ上にデータを確保するための Box の基本を解説しました。
Box::new によるヒープへのデータ確保やアクセス方法といった基本操作に加え、Box が真価を発揮する代表的な活用パターンについて紹介しました。
この仕組みにより、コンパイル時にサイズが確定できないデータ構造や、実行時まで型が定まらない値を安全に扱うことができます。再帰的なデータ構造や、複数のエラー型を統一的に扱いたい場面では、Box の利用を検討してみてください。
上記で紹介しているソースコードについては GitHub にて公開しています。参考にしていただければと思います。







