安全性、速度、並行性を兼ね備えた言語と、巷でうわさの「Rust」を覗いてみる(スマートポインタ編 その1)
“うわさの「Rust」を覗いてみる”シリーズ10回目の今回は、Rustのスマートポインタについて見ていきたいと思います。
スマートポインタとは
とりあえず参考のサイトを読んでいきます。
- ポインタとはメモリのアドレスを指し、Rustで最も一般的なポインタは「参照」
- スマートポインタは通常のポインタよりも多くの機能を持っている
- Rustの標準ライブラリには「参照」よりも高度な機能を持つスマートポインタが多数ある
- 参照はデータを借用するだけのポインタだが、スマートポインタは、指しているデータの所有権を持つことが多い
- 既知のスマートポインタとして、StringやVecが存在するが、これらは普段スマートポインタとは呼ばない
- スマートポインタは通常、構造体を使用して実装され、DerefやDropトレイトを実装することで、参照としての振る舞いや、スコープから外れた際の振る舞いをカスタマイズできる
とりあえず、「参照」より便利なポインタという理解で突き進みます!
ヒープとスタック
今回の内容とは直接関係ないが、ヒープやスタックという言葉が出てくるので、ヒープとスタックについて解説します。
case 1: 動的なデータ構造の場合
リンクドリストやツリー、ハッシュマップなどのデータ構造は、要素数やサイズが実行時に変動する。 これらの動的なサイズ変更をスタックで行うのは不可能なのでヒープを使う。
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert("name", "hoge");
// このHashMapはヒープ上にデータを確保している
}
case 2: 大量のデータ
スタック領域は限られており、大量のデータや大きな配列を確保する場合にはヒープを使用する必要がある。
fn main() {
let array: [u8; 10_000_000] = [0; 10_000_000];
}
/*
thread 'main' has overflowed its stack
fatal runtime error: stack overflow
*/
スタックで大量のデータを確保するとスタックオーバーフローが発生する。
fn main() {
let vec = vec![0; 10000000];
// ヒープ上に確保している
}
ヒープだと問題ない。
Box<T>
Box<T>
は、ヒープ上に値を置くための最も基本的なスマートポインタです。
固定サイズを持たないデータ構造や再帰的なデータ構造を簡単に扱うことができます。
- Box::new関数を使用すると、データをヒープ上に確保することができる
Box<T>
がスコープを抜けると、自動的に解放される(Rustの所有権システムにより、メモリリークを心配することなくヒープを管理できる!(すんばらしい!)Box<T>
はDerefトレイトを実装しているため、中身のデータに直接アクセスすることができる。これにより、Box内のデータを通常の参照のように扱うことができる(参照との扱い方の差を無くしている?)
それでは、実際にコードで確認していきます!
fn main() {
// i32型のデータをヒープに確保
let heap_data = Box::new(42);
// Derefトレイトを実装しているため、直接中身にアクセスできる
println!("Heap data: {}", *heap_data);
// 関数に渡す
hoge(heap_data); // この時点でheap_dataの所有権はhoge関数に移動
// この時点でheap_dataはdropされているので、以下の行はエラーとなる
// println!("Heap data: {}", *heap_data);
//
// error[E0382]: borrow of moved value: `heap_data`
}
fn hoge(data: Box<i32>) {
println!("hoge: {}", *data);
// 関数が終了するとdataもdropされ、ヒープ上のデータも解放される
}
Derefトレイトについて
上記のBot<T>
の説明で出てきた「Derefトレイト」についても確認しておきましょう。
Derefトレイトは、オブジェクトに*演算子を使用してデリファレンスを行う能力を提供します。
Box<T>
は、Derefトレイトを実装しているため、Box<T>の中身を通常の参照のように扱うことができます。
Box<T>
は、すでにDerefトレイトが実装済みなので、学習のために自分で実装してみます。
まずは、Derefトレイトの定義を見ていきましょう。
// lib\rustlib\src\rust\library\core\src\ops\deref.rs
#[lang = "deref"]
#[doc(alias = "*")]
#[doc(alias = "&*")]
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_diagnostic_item = "Deref"]
pub trait Deref {
/// The resulting type after dereferencing.
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_diagnostic_item = "deref_target"]
#[lang = "deref_target"]
type Target: ?Sized;
/// Dereferences the value.
#[must_use]
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_diagnostic_item = "deref_method"]
fn deref(&self) -> &Self::Target;
}
こんな感じになっています。
これを実装してみます。
use std::ops::Deref;
struct Hoge<T>(T);
impl<T> Hoge<T> {
fn new(x: T) -> Hoge<T> {
Hoge(x)
}
}
impl<T> Deref for Hoge<T> {
// Derefトレイト内に定義されている関連型`Target`に`T`型を設定
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
fn main() {
let x = 5;
let y = Hoge::new(x);
assert_eq!(5, x);
assert_eq!(5, *y); // ここでderefメソッドが呼び出される
}
Hogeというタプル構造体に、Derefトレイトを実装してみました。
最後に、BoxのDerefトレイトを確認してみます。
// lib\rustlib\src\rust\library\alloc\src\boxed.rs
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_const_unstable(feature = "const_box", issue = "92521")]
impl<T: ?Sized, A: Allocator> const Deref for Box<T, A> {
type Target = T;
fn deref(&self) -> &T {
&**self
}
}
「&**self」がどうなっているのかを段階的に見ていきます。
- まず、
self
は、&<BoxT, A>
になりBox
への参照 *self
で、Box<T, A>
をデリファレンスして、Box
が指すヒープ上のデータ`T`へのポインタを取得する?- 3で得た
T
型のポインタをデリファレンスしたT型の値に対して、再度、参照を付けて返す?
- 2では
Box<T, A>
に対して、デリファレンスしてるわけで、ここでなぜderefが再帰されないのか不明(#[lang = “owned_box”]で制御されてる?) - 3は結局
T
の参照を返すのに、なぜ2で取得したものを返さないのか(3の処理は必要なのか?)
さいごに
とまあ、見事に沼にはまったので、今の私のRustレベルではここまでということにしておきます。次回に続く。
ところで、「スマートポインタ」ってなんて省略する?やっぱ「スマポ」?個人的には「マトポ」を推していきたい。
この記事を書いた人
-
2008年にアーティスへ入社。
システムエンジニアとして、SI案件のシステム開発に携わる。
その後、事業開発部の立ち上げから自社サービスの開発、保守をメインに従事。
ドメイン駆動設計(DDD)を中心にドメインを重視しながら、保守可能なソフトウェア開発を探求している。
この執筆者の最新記事
関連記事
最新記事
FOLLOW US
最新の情報をお届けします
- facebookでフォロー
- Twitterでフォロー
- Feedlyでフォロー