グローバルナビゲーションへ

本文へ

フッターへ

お役立ち情報Blog



安全性、速度、並行性を兼ね備えた言語と、巷でうわさの「Rust」を覗いてみる(スマートポインタ編 その1)

“うわさの「Rust」を覗いてみる”シリーズ10回目の今回は、Rustのスマートポインタについて見ていきたいと思います。

スマートポインタとは

とりあえず参考のサイトを読んでいきます。

  • ポインタとはメモリのアドレスを指し、Rustで最も一般的なポインタは「参照」
  • スマートポインタは通常のポインタよりも多くの機能を持っている
  • Rustの標準ライブラリには「参照」よりも高度な機能を持つスマートポインタが多数ある
  • 参照はデータを借用するだけのポインタだが、スマートポインタは、指しているデータの所有権を持つことが多い
  • 既知のスマートポインタとして、StringやVecが存在するが、これらは普段スマートポインタとは呼ばない
  • スマートポインタは通常、構造体を使用して実装され、DerefやDropトレイトを実装することで、参照としての振る舞いや、スコープから外れた際の振る舞いをカスタマイズできる

https://doc.rust-lang.org/book/ch15-00-smart-pointers.html

とりあえず、「参照」より便利なポインタという理解で突き進みます!

ヒープとスタック

今回の内容とは直接関係ないが、ヒープやスタックという言葉が出てくるので、ヒープとスタックについて解説します。

スタック
関数の呼び出しに伴い自動的に確保・解放されるメモリ領域。 データはLIFO(Last In First Out)の順番で保存される。 スタックに保存されるデータは固定サイズ。
ヒープ
データのサイズがコンパイル時には決まってなかったり、大量のデータを動的に確保するためのメモリ領域。 ヒープにデータを保存するには、明示的にメモリを確保する必要がある。 使用後は手動で解放する必要があるが、Rustの場合は、所有権システムによって自動で解放される場合が多い。(便利そう?)

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」がどうなっているのかを段階的に見ていきます。

  1. まず、selfは、&amp;<BoxT, A>になりBoxへの参照
  2. *selfで、Box<T, A>をデリファレンスして、Boxが指すヒープ上のデータ`T`へのポインタを取得する?
  3. 3で得たT型のポインタをデリファレンスしたT型の値に対して、再度、参照を付けて返す?
ここで、いくつかの疑問がでてきます。

  • 2ではBox<T, A>に対して、デリファレンスしてるわけで、ここでなぜderefが再帰されないのか不明(#[lang = “owned_box”]で制御されてる?)
  • 3は結局Tの参照を返すのに、なぜ2で取得したものを返さないのか(3の処理は必要なのか?)

さいごに

とまあ、見事に沼にはまったので、今の私のRustレベルではここまでということにしておきます。次回に続く。

ところで、「スマートポインタ」ってなんて省略する?やっぱ「スマポ」?個人的には「マトポ」を推していきたい。

この記事を書いた人

tkr2f
tkr2f事業開発部 web application engineer
2008年にアーティスへ入社。
システムエンジニアとして、SI案件のシステム開発に携わる。
その後、事業開発部の立ち上げから自社サービスの開発、保守をメインに従事。
ドメイン駆動設計(DDD)を中心にドメインを重視しながら、保守可能なソフトウェア開発を探求している。
この記事のカテゴリ

FOLLOW US

最新の情報をお届けします