スマートポインタとボックス――Rustのメモリ管理を理解する:基本からしっかり学ぶRust入門(17)
Rustについて基本からしっかり学んでいく本連載。第17回は、スマートポインタを中心にしたRustにおけるメモリ管理について。
ポインタとスマートポインタ
ポインタという言語仕様は、多くのプログラミング言語に存在します。特にC言語で重要なポジションを占めている印象が強いですが、値の実体を持たずに「場所」のみの情報で値へのアクセスが可能なため、融通性と効率性に優れています。反面、使いこなしが難しいとされ、言語習得の上でのハードルであったり、バグ発生の大きな要因とされたりすることも多い存在です。ただし、使いこなせれば強力な武器となるのは間違いありません。今回は、このポインタがテーマです。
ポインタとしての参照
Rustでもポインタが使えます。それが、「参照」と「スマートポインタ」です。参照は、連載第6回で紹介しました。値を「&」演算子によって借用すると、それは参照となり、所有権を持たずに値へアクセスできる、というものです。これはまさにポインタの働きそのものです。参照は値を指し示すだけなので、その利用においてオーバーヘッドのようなものはありません。その代わり、値を指し示す以外の特別な機能もありません。シンプルなため、関数の引数や戻り値など、コードのさまざまな局面で利用されます。
スマートポインタ
スマートポインタとは、「スマート」(賢い)と名前に付いているように、メモリの管理を自動化したポインタです。もともとC++言語で導入された概念ですが、Rustでも導入されて、メモリ安全において重要な役割を担っています。
本連載ではスマートポインタをすでに使用してきました。文字列(String)やベクター(Vec<T>)などです。String型では保有する文字列の実体はヒープに置き、String型の構造体自身はスタックに置かれて、その場所を保持するだけということを、連載第5回において紹介しました。この際、文字列のためのメモリは自動的に確保され、不要になると自動的に解放されるということにも触れました。このようにスマートポインタは、プログラマーが意識することなく、値のためのメモリを安全に確保、解放できるようになっています。これは、ベクターなどでも同様です。
スマートポインタは、その目的に応じてさまざまな実装があります。既出であるString型、Vec<T>型を含めて、以下に代表的なものを挙げます。
- String:文字列。UTF-8であることを保証されたVec<u8>を保持
- Vec<T>:ベクター。サイズが可変である配列
- Box<T>:ボックス。ヒープへの値の確保を可能にする
- Rc<T>/Arc<T>:参照カウントによって複数の所有者を可能にする
- Cell<T>/RefCell<T>:不変オブジェクトを可変にする
次節では、ベーシックなスマートポインタの実装であるボックス(Box<T>型)を紹介し、これを通じてスマートポインタの動作を掘り下げていきます(Rc<T>/Arc<T>、Cell<T>/RefCell<T>については、最後に概要のみを示します)。なお、このBox<T>型は、連載第15回でエラーを返す汎用的な型として少しだけ登場しました。
【補足】スタックとヒープ
値の格納場所には、大きくスタックとヒープがあります。それぞれに特性があるので、使い分けることになっています。スタックは比較的小さなメモリ領域で、スカラー型や参照、小さな構造体の格納に適しています。スタックに置かれた値は、スコープから外れると自動的に解放されるので管理も容易です。ヒープは大きなメモリ領域で、配列や文字列などのサイズの大きな値の格納に適しています。ただし、ヒープに確保した値は明示的な解放が必要です。スマートポインタは、ヒープからの確保と解放を自動化して、サイズの大きな値の扱いを容易にしているのです。
ボックス化のためのスマートポインタ、ボックス(Box<T>)
Box<T>型は、ボックス化のためのスマートポインタです。ボックス化(Boxing)とは、スカラー値など(この場合はT型の値)を構造体など(この場合はBox<T>型)のインスタンスに変換し、その構造体に実装されるトレイトや関数を利用可能にすることです。Box<T>型は、ベクターと同様にジェネリック型となっています。
スカラー値をボックス化する
一般的な構造体がT型のスカラー値を持つ場合、T型のフィールドを持つのが普通です。しかし、ボックスはスマートポインタになるため、スカラー値自身はヒープに配置され、構造体はその場所を保持します(図参照)。以下では、ボックスはBox<i64>型であるのでヒープにi64型のスカラー値10を格納する領域が確保され、変数x自身はその場所を保持します。println!マクロでその値を表示しています。スコープから外れるとき(main関数から抜けるときに)、ヒープに確保された領域は解放されます。
fn main() { let x: Box<i64> = Box::new(10); println!("{}", x); (1) // 10 }
(1)のように、スマートポインタをprintln!マクロに渡して値が表示されるという動きは、String型でも普通に用いられてきたので、違和感はないでしょう。このような、xがBox<i64>型であるのにもかかわらずヒープにあるはずの「10」を表示するのは、Derefトレイトの働きによるものです(ボックスの中身を取り出すのでボックス化解除〈Unboxing〉とも呼ばれます)。これはスマートポインタの重要な振る舞いの一つですので、のちほど詳しく触れます。
配列をボックス化する
しかしながら、通常は上記のようにボックスを使いません。i64型のフィールドを構造体に持たせればよいですし、フィールドが1個だけなら普通に変数を定義すればいいからです。やはり、C言語においてmalloc関数を使う局面と同様に、スタックに確保するのがはばかれるような巨大な値を使いたい、どれくらい確保すればよいのかが実行してみないと分からない、という場合にボックスは生きてきます。
例えば、要素数の大きな配列です。以下は、要素数10000という大きな配列を初期化し、各要素を表示する例です。
const ARRAY_SIZE: usize = 10000; (1) fn main() { let mut a: Box<[i32; ARRAY_SIZE]> = Box::new([0; ARRAY_SIZE]); (2) let mut i = 1; for elem in a.iter_mut() { (3) *elem = i; i = i + 1; } for elem in a.iter() { (4) print!("{} ", elem); } println!(); // 1 2 3 4 5 6 7 8 9 10 … 9999 10000 }
(1)は、配列の要素数を定数として定義しています。配列の要素数はusize型である必要があるので、型はusizeを明記しています。(2)は、i32型の要素数がARRAY_SIZE個である配列を型とするボックスの定義です。初期値として、要素を全て0で埋めています。これらは、配列を定義する書式の一つです。
(3)は、ボックスに対してiter_mut関数で変更可能な参照を取得し、1からはじまる値を順次書き込んでいます。そして(4)で、それらを全て表示しています。
ここでも、「a.iter_mut()」「a.iter()」というように、ボックスに対して配列の関数を直接呼び出しています。そして、iter_mut関数で取得した参照には、*elemというように値を書き込んでいますが、この「*」は参照外しの演算子です。これらについては、Derefトレイトの項で改めて取り上げます。
【補足】C言語のメモリ管理関数
ここで引き合いに出したmalloc関数は、C言語においてヒープを利用する基本的な関数です。サイズを指定し、確保された領域へのポインタを受け取ります。確保したメモリはfree関数で必ず解放しなければなりません。呼び出しは1対1である必要があり、freeの呼び出しが少なければメモリリークが、多ければダングリングポインタ(有効な場所を指さないポインタ)の問題が発生します。C言語のメモリ管理における課題の一つとして、このmalloc関数とfree関数がしばしば挙げられます。
構造体をボックス化する
構造体をボックスにすることは多いでしょう。連載第5回で紹介したエラー処理においては、複数のエラー型の値をボックスに格納して受け渡していました。
またスカラー値と同様に、配列を含むなどの大きな構造体を使いたい、実行時にならないと構造体の個数が分からないとか、動的に構造体を作成、削除するといった場合にもボックスが生きてきます。
例えば、連結リストや木構造です。これらは各ノードを構造体で実装することが多く、しかもその数は動的に変化しコーディング時には定まらないのが普通です。このようなときにボックスを使うことで構造体を動的に作成できます。
なお、文字列(String型)がそうであるように、ボックスをはじめとするスマートポインタは所有権を持ちます。ですので、ボックス間の値の代入はムーブ(移動)となり、移動元の値は無効になります。このあたりは、第5回と第6回で詳しく触れましたので、特に迷うことはないでしょう。ここでは、スマートポインタは所有権を持ち、値はムーブするということを覚えておいてください。
ボックスで静的なリストを作る
ボックスは、自分自身を含むような構造体(Rustでは列挙体も)の定義にも用いられます。Rustに限らず、コンパイラ型のプログラミング言語では、自分自身を含むような構造体は、コンパイル時にサイズが確定しないので使用できません。そこで、C/C++言語などでは構造体そのものではなくポインタを置くことで、そのようなことを実装していました。Rustでは、スマートポインタであるボックスを使って実装します。
例えば、単方向のリストを考えます。単方向のリストは、リストの各ノードが持つ値とは別に、次のノードへのポインタを持ちます。先頭ノード(ルート)からポインタを順番にたどることで目的のノードにたどり着くことができます。検索時間はO(n)と効率のよいものではありませんが、ノードの挿入や削除はポインタをつなぎ直すだけなので、対象のノードさえ分かれば高速に実行できるというメリットがあります(図参照)。
以下は、静的な単方向リストの定義例です。
enum List { Node(i32, Box<List>), Nil, } …略…
列挙体Listは、列挙子としてNodeとNilをとります。これはリストのノードに相当します。ノードは、Nodeを値として持つか、終端を表すNilのいずれかを値にとるというわけです(ここのNilはただの列挙子なので特別な意味はありません)。Nodeは、i32型の値とボックス(Box<List>)の値をとります。ボックスはポインタであるので、列挙体Listのサイズはコンパイル時に確定します。
以下は、上記の続きで、列挙体Listをインスタンス化し、順番に値を表示する例です。
…略… use List::{Node, Nil}; (1) fn main() { let list = Node(0, (2) Box::new(Node(1, Box::new(Node(2, Box::new(Nil)))))); let mut l = &list; (3) loop { match &*l { Nil => { println!("Nil"); break; }, Node(value, next) => { println!("{}", value); l = &next; }, } } }
実行結果は、下記です。先頭ノードからNilまで順番に表示されています。
0 1 2 Nil
(1)は、NodeとNilの記述をシンプルにするためのuse文です。そして、(2)がMyListのインスタンス化、(3)がその表示です。(2)では、リストのノードを静的に定義しています。(3)以降でリストの各ノードを表示しています。列挙体を扱うので、値の判定にmatch式を使っているため複雑に見えますが、以下の2パターンになります。Nilにマッチした場合は「Nil」と表示し繰り返しを中断します。Nodeにマッチした場合はその値を表示し、次のボックスを参照として取得し、繰り返しを継続します。ここでは表示の例ですが、リストの走査についても同様の繰り返しを用いることになります。
このような、列挙体の定義や参照の方法については連載第7回で紹介しています。
ボックスで動的なリストを作る
前項ではボックスで単方向リストを作りましたが、コーディング時にノードの数や値が決まっていることはまれです。そこで、動的なリストをボックスで作ってみます。とはいえ、列挙子とボックスを使う点は一緒で、幾つかの関数を列挙体に追加するだけです。ここでは、リストの先頭に新しいノードを挿入する例を紹介します。
…略… impl List { fn new() -> List { (1) Nil } fn prepend(self, elem: i32) -> List { (2) Node(elem, Box::new(self)) } } …略… fn main() { let mut list = Node::new(); (3) list = list.prepend(0); list = list.prepend(1); list = list.prepend(2); …略… }
実行結果は、下記です。先頭ノードからNilまで順番に表示されていますが、最後に挿入したものが最初に表示されている点に要注意です。
2 1 0 Nil
(1)では、List列挙体にnew関数を実装しています。Nilを返すので、終端のみのリストがこれで生成できます。(2)は、リストの先頭にノードを挿入するprepend関数の実装です。引数で指定される値を持つボックスを返すだけですが、自分自身(self)をボックスの初期値として渡しているので、自分自身を指すボックスが新たに作成される、というわけです。(3)以降は、動的にリストを生成、ノードを3つ挿入しています。
スマートたらしめるDerefトレイトとDropトレイト
ここまで、ボックスの振る舞いを通じてスマートポインタの動きを見てきました。その際、Derefトレイトがたびたび登場しました。ここではスマートポインタをスマートたらしめるこのDerefトレイトと、Dropトレイトを紹介します。独自のスマートポインタを定義するときは、この2つのトレイトの実装は必須です。
Derefトレイト
Derefトレイトは、スマートポインタを参照として振る舞わせるために必要な実装を含んでいます。Derefトレイトにより、スマートポインタと参照を受け取るような関数を簡単に実装できますし、スマートポインタが参照されたときの振る舞いを定義できます。Derefとは、de-referenceといった意味で、「参照外し」と呼ばれます。参照外しを理解するために、普通の参照を用いてこれを説明します。
以下は、変数aと、それへの参照である変数rを宣言し、それぞれprintln!マクロで表示しています。
fn main() { let a = 100; let r = &a; (1) println!("a = {}, r = {}", a, *r); (2) // a = 100, r = 100 }
(1)では、「&a」としてaへの参照を取得してrの初期値としています。そして(2)では、「*」演算子を用いて参照を解決しています。これが参照外しで、「*」は参照外し演算子です。C言語などで、「*」がポインタの指す場所の値にアクセスするのと似た目的を持った演算子です。
ちなみに、以下のように「*」を取り去ってもサンプルは正しく動作します。
println!("a = {}, r = {}", a, r); // a = 100, r = 100 }
これが動作するのは、変数rに対して「強制参照外し」が働いているからです。つまり、この文脈では参照外しが望ましいということになると、「*」がなくても参照外しを自動的に(強制的に)適用します。
以下は、ボックスを使った参照外しのサンプルです。
fn main() { let a = Box::new(100); (1) let r = &a; (2) println!("a = {}, r = {}", a, *r); (3) // a = 100, r = 100 println!("a = {}, r = {}", a, r); (4) // a = 100, r = 100 }
(1)は普通のボックスを作成していますが、(2)でその参照を取得しています。(3)で「*」による参照外しが、(4)で強制参照外しが働いているのが分かります。
ここで、Derefトレイトの動きを掘り下げてみます。本節冒頭のsrc/bin/box_scalar.rsでは、println!マクロでBox<T>型の値xを直接表示できていました。これは、println!マクロの呼び出しで強制参照外しが働き、xに対して参照外しが行われたからです。このとき、内部的にはおおまかに以下のコードが実行されています。
*(x.deref())
このderef関数は、Derefトレイトの関数で、以下のような実装になっています。
fn deref(&self) -> &T { &self.0 }
&self.0とあるように、自分自身の0番目のフィールドを参照として返しています。この書式には見覚えがあると思いますが、タプルのフィールドへのアクセスです。タプルは構造体の一種ですが、構造体と異なり各フィールドが名前を持たないので、このようにインデックスでアクセスします。つまり、ボックスは内部的にはタプルだということです。実際には、ボックスにおけるタプルのフィールドはprivateすなわち不可視になっているので、フィールドに直接アクセスすることはできません。その代わり、deref関数を使って間接的にアクセスするのです。
deref関数によって、「*」による参照外しや強制参照外しは、自動的にボックスの持つ値へ作用します。なお、deref関数は明示的に呼び出すことはできません。参照外しが必要な場合に自動的に呼び出されるのみです。
Dropトレイト
Dropトレイトは、連載第5回で示したように、値がスコープから外れる際に自動的に呼び出されるdrop関数を実装しています。これにより、ヒープ上に確保されていた領域が安全に解放されるというスマートポインタの動作を実現しています。
【補足】std::memモジュール
deref関数と同様にdrop関数は明示的に呼び出すことはできません。これはすなわち、スコープの終了までインスタンスを保持する必要がなくても、それを強制的に破棄できない、ということです。強制的な破棄が必要な場合には、std::memモジュールのdrop関数を呼び出すことで破棄できますが、これはより直接にメモリを操作する関数群の一部です。std::memモジュールの関数群は、使い方次第ではきめ細かなメモリ操作が可能ですが、メモリへの格納イメージを意識する必要があるなど、難易度は少し高めです。
その他のスマートポインタ、Rc<T>とArc<T>、Cell<T>とRefCell<T>
ここまで、ボックス(Box<T>)を用いて、スマートポインタの基本的な振る舞いを紹介してきました。ここでは、その他のスマートポインタとして、参照カウントのあるRc<T>型とArc<T>型、書き換え可能なCell<T>とRefCell<T>型を簡単に紹介します。
参照カウントを持つスマートポインタ
Rc<T>型とArc<T>型は、参照カウントを持つ以外はボックスと同様の機能を持つスマートポインタです。Rc<T>とArc<T>の違いは、Arc<T>がRc<T>のスレッドセーフ版であるということです。RcとはReference Countの意味で、文字通り参照数を保持する機能を持ったスマートポインタです。単方向リストのようにノードの所有が1カ所からならボックスでよいのですが、グラフや双方向リストのようにノードの所有が複数箇所からになる場合には、所有権の関係でボックスで実現することは困難です。このようなときに、Rc<T>/Arc<T>型を使います。Rc<T>/Arc<T>型によって、所有者が幾つあるかということを常に把握して、所有者がいる限りはインスタンスを破棄しないということが可能になります。
書き換え可能なスマートポインタ
Cell<T>とRefCell<T>型は、内部の値を書き換えることのできるスマートポインタです。Rc<T>/Arc<T>型と組み合わせて使うことで、複数の所有者がいる場合でも安全に値を書き換えて、共有できます。値を書き換えるということは参照でも可能でしたが、連載第6回で紹介したように、変更可能な参照は1個のみ許されるという制約がありました。Cell<T>型は、ゲッタ(get関数)とセッタ(set関数)を持っており、安全な値の取得と変更を保証してくれます。反面、値への参照が取得できない、Copyトレイトの実装が必要、スレッドセーフでないなどの大きな制約があります。機能は強力だが使える局面は限られる、という問題があるようです。RefCell<T>型は、その名の通り参照を取得できるようにしたものです。ゲッタとセッタを持たない代わりに、変更可能な借用を取得できるなど制限が緩くなっています。このため、RefCell<T>型が使われることが多いようです。
まとめ
今回は、Rustにおけるメモリ操作として、スマートポインタをボックスを中心に紹介しました。
次回は最終回として、マクロをはじめとするRustを特徴付けるさまざまな未紹介の機能を紹介します。
筆者紹介
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・Twitter: @yyamada(https://twitter.com/yyamada)
・Facebook(https://www.facebook.com/WINGSProject)
Copyright © ITmedia, Inc. All Rights Reserved.