Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

一意ポインター

Rust はシステム言語であるため、メモリへの生のアクセスを提供しなければなりません。 これは(C++ と同様に)ポインターを介して行われます。ポインターは、構文とセマンティクスの両面で Rust と C++ が大きく異なる領域の 1 つです。Rust はポインターを型チェックすることで メモリ安全性を強制します。これは他の言語に対する Rust の大きな利点の 1 つです。 型システムは少し複雑ですが、その見返りとしてメモリ安全性とベアメタル級の性能を得られます。

Rust のポインターをすべて 1 つの記事で扱うつもりでしたが、このテーマは大きすぎると思います。 そのため、この記事では 1 種類、すなわち一意ポインターだけを扱い、その他の種類は後続の記事で扱います。

まず、ポインターを使わない例です。

#![allow(unused)]
fn main() {
fn foo() {
    let x = 75;

    // ... `x` を使って何かを行う ...
}
}

foo の終わりに到達すると、(C++ と同様に Rust でも)x はスコープを抜けます。 これは、その変数にはもうアクセスできず、その変数用のメモリは再利用できることを意味します。

Rust では、任意の型 T について、T への所有(別名、一意) ポインターを Box<T> と書けます。Box::new(...) を使うと、ヒープ上に領域を割り当て、 その領域を指定された値で初期化します。これは C++ の new に似ています。 例を示します。

#![allow(unused)]
fn main() {
fn foo() {
    let x = Box::new(75);
}
}

ここで x は、値 75 を含むヒープ上の場所へのポインターです。 x の型は Box<i32> です。let x: Box<i32> = Box::new(75); と書くこともできました。これは C++ で int* x = new int(75); と書くのに似ています。 C++ と異なり、Rust はメモリを自動的に後片付けしてくれるため、 freedelete を呼び出す必要はありません1。一意ポインターは値と同様に振る舞います。 つまり、変数がスコープを抜けると削除されます。この例では、 関数 foo の終わりで x にはもうアクセスできなくなり、x が指していたメモリは 再利用できるようになります。

所有ポインターは C++ と同様に * を使ってデリファレンスします。例を示します。

#![allow(unused)]
fn main() {
fn foo() {
    let x = Box::new(75);
    println!("`x` points to {}", *x);
}
}

Rust のプリミティブ型と同様に、所有ポインターとそれが指すデータは デフォルトでイミュータブルです。C++ と異なり、イミュータブルなデータへのミュータブルな(一意) ポインターや、その逆を持つことはできません。データのミュータビリティはポインターに従います。 例を示します。

#![allow(unused)]
fn main() {
fn foo() {
    let x = Box::new(75);
    let y = Box::new(42);
    // x = y;         // 許可されません。x はイミュータブルです。
    // *x = 43;       // 許可されません。*x はイミュータブルです。
    let mut x = Box::new(75);
    x = y;            // OK。x はミュータブルです。
    *x = 43;          // OK。*x はミュータブルです。
}
}

所有ポインターは関数から返され、その後も生存し続けることができます。 返された場合、そのメモリは解放されません。つまり、Rust にはダングリングポインターがありません。 メモリがリークすることもありません。ただし、いずれスコープを抜け、そのときに解放されます。 例を示します。

#![allow(unused)]
fn main() {
fn foo() -> Box<i32> {
    let x = Box::new(75);
    x
}

fn bar() {
    let y = foo();
    // ... y を使う ...
}
}

ここでは、メモリは foo で初期化され、bar に返されます。xfoo から返されて y に格納されるため、削除されません。bar の終わりで y がスコープを抜けるため、 メモリは回収されます。

所有ポインターが一意(線形とも呼ばれます)なのは、任意のメモリ領域に対して、 どの時点でも(所有)ポインターは 1 つしか存在できないからです。これは ムーブセマンティクスによって実現されます。あるポインターが値を指すと、それ以前のポインターには もうアクセスできなくなります。例を示します。

#![allow(unused)]
fn main() {
fn foo() {
    let x = Box::new(75);
    let y = x;
    // x にはもうアクセスできない
    // let z = *x;   // エラー。
}
}

同様に、所有ポインターが別の関数に渡されたり、フィールドに格納されたりすると、 それにはもうアクセスできません。

#![allow(unused)]
fn main() {
fn bar(y: Box<isize>) {
}

fn foo() {
    let x = Box::new(75);
    bar(x);
    // x にはもうアクセスできない
    // let z = *x;   // エラー。
}
}

Rust の一意ポインターは C++ の std::unique_ptr に似ています。Rust では C++ と同様に、 値への一意ポインターは 1 つしか存在できず、その値はポインターがスコープを抜けると削除されます。 Rust はチェックの大部分を実行時ではなく静的に行います。そのため C++ では、 値がムーブされた一意ポインターにアクセスすると(それは null になるため)実行時エラーになります。 Rust ではこれはコンパイル時エラーとなり、実行時に誤ることはありません。

後で見るように、Rust では一意ポインターの値を指す他のポインター型を作成できます。 これは C++ に似ています。しかし C++ では、解放済みメモリへのポインターを保持することで 実行時にエラーを引き起こせます。Rust ではそれは不可能です(Rust の他のポインター型を扱うときに、 どのようにしてそうなるのかを見ます)。

上で示したように、所有ポインターはその値を使うためにデリファレンスする必要があります。 しかし、メソッド呼び出しでは自動的にデリファレンスされるため、メソッド呼び出しに -> 演算子を使ったり * を使ったりする必要はありません。この点で、Rust のポインターは C++ のポインターと参照の両方に少し似ています。例を示します。

#![allow(unused)]
fn main() {
fn bar(x: Box<Foo>, y: Box<Box<Box<Box<Foo>>>>) {
    x.foo();
    y.foo();
}
}

Foo がメソッド foo() を持っていると仮定すると、これらの式はどちらも OK です。

既存の値を指定して Box::new() を呼び出しても、その値への参照を取るのではなく、 その値をコピーします。したがって、

#![allow(unused)]
fn main() {
fn foo() {
    let x = 3;
    let mut y = Box::new(x);
    *y = 45;
    println!("x is still {}", x);
}
}

一般に、Rust はコピーセマンティクスではなくムーブセマンティクスを持ちます (一意ポインターで上に見たとおりです)。プリミティブ型はコピーセマンティクスを持つため、 上の例では値 3 がコピーされますが、より複雑な値ではムーブされます。 これについては後でより詳しく扱います。

ただし、プログラミングでは値への参照が複数必要になることがあります。 そのために、Rust には借用ポインターがあります。それらについては次の記事で扱います。


  1. C++11 で導入された std::unique_ptr<T> は、いくつかの点で Rust の Box<T> に似ていますが、重要な相違点もあります。

    類似点:

    • C++11 の std::unique_ptr<T> と Rust の Box<T> が指すメモリは、 std::unique_ptr<T> がスコープを抜けると自動的に解放されます。
    • C++11 の std::unique_ptr<T> と Rust の Box<T> はどちらもムーブセマンティクスのみを示します。

    相違点:

    1. C++11 では、既存のポインターから std::unique_ptr<T> を構築できるため、 同じメモリに対する複数の一意ポインターを許してしまいます。 この振る舞いは Box<T> では許可されません。
    2. 別の変数や関数にムーブされた std::unique_ptr<T> をデリファレンスすると、 C++11 では未定義動作を引き起こします。Rust ではこれはコンパイル時に検出されます。
    3. ミュータビリティまたはイミュータビリティは std::unique_ptr<T> を「通じて」 伝播しません。つまり、const std::unique_ptr<T> をデリファレンスしても、基になるデータへの ミュータブルな(非 const の)参照が得られます。Rust では、イミュータブルな Box<T> はそれが指すデータの変更を許可しません。

    Rust の let x = Box::new(75) は、C++11 では const auto x = std::unique_ptr<const int>{new int{75}};、C++14 では const auto x = std::make_unique<const int>(75); と解釈できます。