Rust入門

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

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

Rust でヒープ上にデータを確保するための Box の基本を初心者にも分かりやすく解説します。

Box 型の概要

Box 型とは

Rust では、スタック領域にデータが格納される基本型と StringVec のように、内部的にヒープ領域に確保される型があります。Boxは、特定の用途に限定された型とは異なり、任意の型をヒープに確保することができる型です。

C/C++ 言語などで、データの格納しているメモリ領域を指し示すものをポインタと言いますが、Rust では、データの所有権と追加機能を持つポインタが用意されており、これをスマートポインタと言います。Box は、最も基本的で汎用的なスマートポインタです。

Box 型の主な特徴は以下の通りです。

  • ヒープ上にデータを確保する。
  • 所有権を持ち、スコープを抜けると自動的にメモリが解放される
  • スタック上のサイズは、ポインタ分のサイズに固定できる

Rust では、通常の基本型はスタックにデータを確保しますが、Box 型を使うことで明示的にヒープへデータを配置することもできます。

この記事では、Box 型の基本的な生成方法から、具体的な使用例を紹介します。

なぜ Box が必要なのか

まずは、そもそも Box がなぜ必要になるのかを考えてみましょう。

Rust では変数のサイズをコンパイル時に確定している必要があります。しかし、以下のようなケースではコンパイル時にサイズを確定することができません。

  • 型が自分自身を再帰的に参照する構造(連結リストなど)
  • 実行時までどの具体的な型が入るか分からない場合(トレイトオブジェクトなど)

こうしたケースでは、実データをヒープ上に配置し、スタック上には固定サイズのポインタデータだけ持つようにすることでコンパイルができない問題を回避できます。これを実現できるのが Box 型です。

なお、Box 型は所有権の仕組みをそのまま利用できるため、スコープを抜けるとヒープ上のデータは自動的に開放され、Rust の特徴であるメモリ安全性が確保されます。

所有権の基本については「【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! では、参照外しをしていませんが、BoxDisplay トレイトで参照外しが行われるため、そのまま 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 トレイトを実装した CircleRectangle を 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

例における CircleRectangle はそれぞれ異なるサイズ・実装を持つ型ですが、いずれも Shape トレイトを実装しています。このような場合には、Vec<Box<dyn Shape>> のようにすることで、Shape トレイトを実装している型を同じ Vec に格納することができます。

Vec では、格納する要素は同じサイズでなければならないという制約がありますが、Box<dyn Shape> は、ヒープ上の実データへのポインタであるため固定サイズの値となり、この制約を満たすことができます。

まとめ

Rust でヒープ上にデータを確保するための Box の基本を解説しました。

Box::new によるヒープへのデータ確保やアクセス方法といった基本操作に加え、Box が真価を発揮する代表的な活用パターンについて紹介しました。

この仕組みにより、コンパイル時にサイズが確定できないデータ構造や、実行時まで型が定まらない値を安全に扱うことができます。再帰的なデータ構造や、複数のエラー型を統一的に扱いたい場面では、Box の利用を検討してみてください。

ソースコード

上記で紹介しているソースコードについては GitHub にて公開しています。参考にしていただければと思います。

あわせて読みたい
Rust プログラミング入門
Rust プログラミング入門

ABOUT ME
ホッシー
ホッシー
システムエンジニア
はじめまして。当サイトをご覧いただきありがとうございます。
私は製造業のメーカーで、DX推進や業務システムの設計・開発・導入を担当しているシステムエンジニアです。これまでに転職も経験しており、以前は電機メーカーでシステム開発に携わっていました。

これまでの業務を通じてさまざまなプログラミング言語や技術に触れてきましたが、その中でもRustの設計思想に惹かれ、この言語についてもっと深く学びたい、そしてその魅力を発信していきたいと思い、このサイトを立ち上げました。

自身の学びを整理しつつ、同じようにRustに興味を持つ方のお役に立てるような情報を発信していければと思っています。どうぞよろしくお願いいたします。

※キャラクターデザイン:ゼイルン様
記事URLをコピーしました