[Rust] スマートポインタの基礎

2022.01.21

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Introduction

最近はRustを使う機会がたまにあるのですが、
もともと仕事ではJavaやJavaScriptを使うことが多かったので
そういった言語との違いには少々苦労しています。

Rustを学習するにあたって壁となると思われるポイントはいくつかありますが、
その中のひとつ、スマートポインタについて自分が学んだ結果を記述します。

なお、本稿サンプルコード実行についてはRustのPlayGroundにて行いました。

Smart Pointer?

ポインタというと、メモリのアドレスを含む変数の一般的な概念です。
C/C++などの言語に慣れている人ならよく知っていると思います。
この「アドレス」というやつは、何かの他のデータを参照したり指し示したりします。

Rustで最も使用されるポインタは↓のような「参照」です。
&記号を用いて指している値を借用することが可能です。
それ以外の特殊機能はありません。

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

※ 参照についてはこのへんとかを確認

それに対してスマートポインタとは、メモリアドレスをデリファレンス(*で参照するやつ)でき、
さらに固有の特殊能力を持つポインタ型です。

※ スマートポインタという概念はRust特有でなくC++発で他言語にも存在している

Rustでは、いろいろなスマートポインタが標準ライブラリに存在しています。
代表的なスマートポインタの例としては、

  • String : 有効なUTF8の配列を保証されたバイトのVecとして保持
  • Vec : サイズを変更可能な配列
  • Box : ヒープへ値の確保が可能
  • Rc/Arc : 参照カウントで複数の所有者を実現
  • Cell/RefCell : 不変オブジェクトを可変にする機能を持つ

などがあります。
StringやVecはRustのドキュメントでもよく使われてますね。
以降のセクションで代表的なスマートポインタについて紹介していきます。

Stack & Heap

スタック

Rustのプロセス起動時に割り当てられたメモリ領域で、データを保持するための構造です。

LIFO形式(最後に入った要素が最初に取り出される)で
任意のサイズの要素をいれることができます。
できる操作はpush(スタックに要素を追加)とpop(スタックから要素を除去)だけです。

スタックは、スタックフレーム(アロケーションレコードともいわれる)とデータで構成されます。
スタックフレームは関数の実行とともに作られ、
現在のスタックフレームを指しているアドレスを反映させます。
これがスタックポインタとよばれます。

関数内で関数を呼び出すたびにスタックポインタはその数を減らし、
関数からリターンすることで増えます。 システムのスタックフレームをつかいきってしまったとき、
スタックオーバーフローが発生します。

※ スタックポインタの増減については環境依存

スタックフレームでは、関数が呼ばれている間その関数の状態が保持されます。
関数内で関数をよぶと、呼び出し側の関数の状態はそのままの状態になります。

スタックは関数がよばれるとそこにローカル変数用のスペースを作り、
その関数が使う変数すべてがメモリ上にとなりあって
配置されるのでアクセスが速いのです。

Rustでは基本的にすべての値はスタックに保存されます。

ヒープ

ヒープは、プログラムのコンパイル時にサイズが不明な型のためのメモリ領域です。
メモリを割り当てるにはOSに対して割り当てを依頼するため、
スタックより低速になりますが、容量はスタックより大きいです。
VecやString、Boxなどの可変サイズのデータがヒープに保存されます。

また、ヒープのデータはポインタでアクセスする必要があります。

スタック or ヒープ?

基本的には スタックへのアクセスは速く、ヒープは遅い
ので、「迷ったらスタックを使う」のがよいみたいです。
詳説Rustプログラミングより

ただ、データをスタックにおくには、コンパイル時に型のサイズを知っている必要があるので注意。
(Sizedを実装する型を使えばスタックに保存される)

Rustにおけるスタックとヒープの説明についてはここがわかりやすかったです。

では次に、代表的なスマートポインタについて説明します。

Box

Boxはシンプルなスマートポインタです。
Box型を作成すると、任意の値をヒープに割り当てることができます。

let foo:Box<i32> = Box::new(1);

Boxはヒープに割り当てられた値へのスマートポインタです。
対象スコープを抜けるとオブジェクトが破棄されてメモリが解放されます。

let x:i32  = 1; //スタック
let y:Box<i32> = Box::new(2); //ヒープ
println!("x + y = {}", x + *y);

Arc/Rc

ArcおよびRcは参照カウント方針のスマートポインタです。
これを使用すると、対象データに複数の所有者を持たせることができるというやつです。
Arc/Rcは所有者の数をトレースし、所有者がいなくなった時点でデータを除去してくれます。

なお、ArcとRcの違いはスレッドセーフか否かです。
Rcはスレッドセーフではないので複数スレッドで共有することはできません。

↓のコードを実行すると、Rc型のfooをcloneした時点で参照カウントが増えます。
また、fooとbar2つの変数はおなじアドレスを示しています。

use std::rc::Rc;

fn main() {
    let foo = Rc::new(50);

    // reference count = 1
    println!("reference count :{}",Rc::strong_count(&foo));

    let bar = Rc::clone(&foo);

    // reference count = 2
    println!("reference count :{}",Rc::strong_count(&bar));

    // &foo == &bar
    println!("foo = {:p}", foo);
    println!("bar = {:p}", bar);
}

しかし、Rcだけでは状態の共有はできても
値を書き換えることはできません。
状態を共有をした変数を書き換えるには、後述するCellを使います。

Cell/RefCell

Cellはさきほど説明したBoxと同じく、任意の型を指定してラップするオブジェクトです。
違いはgetとsetメソッドを提供してくれることです。

let foo : Cell<i32> = Cell::new(100);
foo.set(200);
println!("foo : {}", foo.get());  // foo = 200

変数をmutで宣言していないのに値が書き換わってます。
Cellは、mutで宣言していなくてもsetで値を変更することができます。

さきほどのRcを使ったサンプルを書き換え可能にすると
↓のようになります。

use std::cell::Cell;
use std::rc::Rc;

fn main() {
    let foo:Rc<Cell<i32>> = Rc::new(Cell::new(50));

    // reference count = 1
    println!("reference count :{}",Rc::strong_count(&foo));

    let bar = Rc::clone(&foo);

    // reference count = 2
    println!("reference count :{}",Rc::strong_count(&bar));

    // &foo == &bar
    println!("foo = {:p}", foo);
    println!("bar = {:p}", bar);

    println!("before foo value:{}",(*foo).get());
    println!("before bar value:{}",(*bar).get());

    // update value
    (*foo).set((*foo).get() + 1);

    println!("after foo value:{}",(*foo).get());
    println!("after bar value:{}",(*bar).get());

}

mutにしてない変数の値を更新することができ、かつ状態を共有しています。
すごい機能をもったCellですが、

  • Cellの中の参照が取得不可能
  • Cellの中の型はCopyトレイト実装が必須
  • 複数スレッドでやりとりできない

などのかなり厳しい制限があります。
そういう場合、Cellより少し制限の緩いRefCellを使うと良いみたいです。

RefCellは、中の型はSizedを実装してればOKですし、中の参照も取得可能です。
ただ、Cellには存在したget/setは存在せず、Ref型を返すborrow、
RefMutを返すborrow_mutメソッドを使用する必要があります。
それ以外にも注意点がありますので、このへんを参照。

※ 内部可変性、Cell、RefCellについてはここがわかりやすかったです

Summary

本稿ではRustで少しつまづきそうなスマートポインタの基礎について解説しました。
効率のよいメモリの扱い方やRustの制限を安全にかわす方法など、
本格的にRustを使っていくにあたって必須となりそうなので、
しっかり理解したいところです。

References