スマートポインタ

スマートポインタとは

Rustにおけるスマートポインタとは内部に値を持ち、C言語のポインタのような振る舞いをもつデータ構造であり、唯一ヒープ上で値が確保される

その他のRustの値は全てスタック上で確保される。

以下の2つのトレイトが実装されている必要がある。

  • Deref トレイト
  • Drop トレイト

Deref トレイト

内部で所持している値を返す。

このトレイトを実装すると、自動的に* 演算子が実装される。

NOTE:

  • *演算子について
    • 参照外し演算子と呼ばれる。(Dereference operator)
    • 参照型(&T, &mut T)に使用すると、参照元の値が返される
  • &演算子について
    • 参照強制演算子と呼ばれる
    • 値から参照型もしくはポインタ型を作ることが出来る

参照型における *演算子

参照元の値を表す。

let a = [0, 1, 2];

let a = [0, 1, 2];
let iter  = a.iter().map(|x| *x < 2).filter(|x| *x == true);

NOTE: 比較演算子T型&T型 の比較ができない

したがって、* 演算子(参照外し演算子をつけなければコンパイルエラーとなる。

error[E0308]: mismatched types
   |
14 | let iter  = a.iter().map(|x| x < 2)
   |                                  ^
   |                                  |
   |                                  expected reference, found integer
   |                                  help: consider borrowing here: `&2`
   |
   = note: expected reference `&_`
                   found type `{integer}`

スマートポインタにおける *演算子

内部で所持している値(スライス)を返す。

let vec = vec![1,2,3,4,5];
let _arr = &*vec; // &[i32]

let string = "Hello World".to_string();
let _str = &*string; // &str

NOTE: & を付けている理由

Rustでは必ずコンパイル時にサイズが決まっている必要があるため。 & を付けないとコンパイルエラーになる。

error[E0277]: the size for values of type `[{integer}]` cannot be known at compilation time
  |
5 | let _arr = *vec; // [i32]
  |     ^^^^ doesn't have a size known at compile-time
  |
  = help: the trait `std::marker::Sized` is not implemented for `[{integer}]`

Dropトレイト

自身のスコープが切れた際に、内部で保持している値のクリーンナップ処理などを行う。

代表的なスマートポインタ

標準ライブラリーで実装されている代表的なスマートポインタ

  • String
  • Vec<T>
  • Box<T> ヒープ領域への値の確保
  • Rc<T> 参照カウントによる複数の所有者を実現
  • RefCell<T> 借用ルールをランタイム時に強制させる

Box<T>

値をヒープ領域に格納するためのスマートポインタ

これによって以下を実現することができる。

  1. 再帰データ型の実現
  2. 所有権の移動による巨大データのコピー回避
  3. トレイトオブジェクトへの所有権の付与

1. 再帰データ型の実現

Rustではコンパイル時に型のサイズが決まっている必要があるので、 Consリストのような再帰型をそのままでは実現できない。

enum List {
    Cons(i32, List),
    Nil,
}

error[E0072]: recursive type `List` has infinite size
  |
2 | enum List {
  | ^^^^^^^^^ recursive type has infinite size
3 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
  = help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to make `List` representable

f:id:yossan2:20200920184512p:plain

そこで Box\<T> を間に挟むことで、型のサイズを固定にすることができる

enum List {
    Cons(i32, Box<List>),
    Nil,
}

let item1 = Box::new(List::Nil);
let item2 = Box::new(List::Cons(1, item1));
let item3 = Box::new(List::Cons(2, item2));
let item4 = Box::new(List::Cons(3, item3));

f:id:yossan2:20200920184507p:plain

2. 所有権の移動による巨大データのコピー回避

巨大なデータをもつ値をBox<T>に包み込むことで、所有権の移動でも値そのもののコピーを回避できる

let box1 = Box::new(big_data);

....

let box2 = box1; // 所有権が移っても big_data のコピーは発生しない (ポインタがコピーされるだけ)

3. 所有権の管理

具体的な型情報が消去された状態でトレイトオブジェクトの所有権を管理できる。

fn print_if_string(value: Box<dyn Any + Send>) {
    if let Ok(string) = value.downcast::<String>() {
        println!("String ({}) : {}", string.len(), string);
    }
}

// 値をBox<_>型として保持できる
let my_string= Box::new("hello".to_string());
let my_u8 = Box::new(5u8);

print_if_string(my_string);
print_if_string(my_u8);
  • dyn Any + Send 何らかのstruct型であることを表す
  • Box<T>::downcast() Result<Box<T>, Box<dyn Any + 'static>> を返す
    • downcast: トレイトオブジェクトから型への変換

トレイトオブジェクトとの違い

トレイトオブジェクトは、動的ディスパッチになるため、ランタイムでコストが発生する。 Box<T>インスタンスは、静的ディスパッチであるため、型を変換するオーバーヘッドが発生しない。

関連

yossan.hatenablog.com

参照

Rust における `From<T>` とか `Into<T>` とかの考え方 - Qiita コメント

ボクシングされたトレイトオブジェクトの利点は、具体的な型情報が消去された状態でトレイトオブジェクトの所有権を管理できるという点です。 なので、逆に言えば所有権を管理する必要がなければボクシングは必要ありません。