2016年3月1日火曜日

C++プログラマー向けRust 翻訳シリーズ4

固有ポインタ


Rustはシステム(記述用)言語だ。すなわち、特定メモリアドレスへのアクセスができなければならない。これをRustでは、(C++同様)ポインタを介して行う。
ポインタは、見た目にも意味的にもRustとC++で差異の多い領域だ。Rustでは、メモリ安全性の担保のため、型チェックを伴うポインタを強制される。これは、Rustが他言語に勝る特徴の一つだ。Rustの型システムは少々複雑だが、その代わり、メモリ安全性と純度の高いパフォーマンスを得ることができる。

1回、1投稿でRustのポインタ全てを網羅しようとしたことがあったが、それでは手に負えないような話題だ。なので、今回の投稿では1種類(unique pointers)のみに的を絞り、他のポインタは後々の投稿に譲ることにしよう。

手始めにポインタなしの例だ。

fn foo() {
    let x = 75;

    // xに対する処理...
}
foo関数の末端まで到達すると、変数xはスコープ外になる(C++と同じくRustでも)。これは要するに、この変数がアクセスできなくなり、そのメモリ領域を再利用できるようになるということだ。

Rustでは、あらゆる型Tに対して、Box<T>と書いて所有権あり(あるいは固有)ポインタを表すことができる。Box::new(...)と書けば、ヒープ領域にメモリーを確保し、初期値を与えることができる。これはC++のnew演算子に近い。

fn foo() {
    let x = Box::new(75);
}
ここで、変数xはヒープ領域上に75という値を持つメモリー領域を指すポインタだ。変数xの型はBox<int>let x: Box<int> = Box::new(75);と書くこともできる。この記法は、C++のint* x = new int(75);という書き方に近い。
C++と異なり、Rustでは後処理を自動的に行ってくれるので、free関数やdelete演算子を呼び出す必要はない。
固有ポインタは、値と似た挙動をする。すなわち、変数がスコープ外に抜けると、削除されるのだ。前述の例では、関数fooの末端で変数xはアクセスできなくなり、そのメモリー領域は再利用可能になる。

所有権ありのポインタは、C++同様、*演算子を使って値を取ること(dereference)ができる。

fn foo() {
    let x = Box::new(75);
    println!("`x` points to {}", *x);
}
Rustの組み込み型同様、所有権ありポインタとその中身は、デフォルトで不変になる。C言語と違い、不変データを参照する可変で所有権ありのポインタを作ることはできず、その逆もまた然りである。データの可変性は、ポインタにならう。

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にはnullポインタはないのだ。メモリーリークも発生しない。
しかし、いつかはスコープ外に抜け、メモリー領域が解放される。

fn foo() -> Box<i32> {
    let x = Box::new(75);
    x
}

fn bar() {
    let y = foo();
    // yを使う...
}
ここで、メモリー領域が関数foo内で初期化され、関数barに返される。変数xは関数fooの戻り値になり、変数yになるため、そのメモリー領域が削除されることはない。
関数barの末端で、変数yがスコープ外に抜け、メモリー領域が再利用可能になる。

所有権ありのポインタは、固有(連続的とも言える)だ。いかなる時でも、あるメモリー領域を指すポインタは一つしか存在できないからだ。
これは、Move機構により実現されている。あるポインタがデータを指し示したら、直前のいかなるポインタもアクセスできなくなる。

fn foo() {
    let x = Box::new(75);
    let y = x;
    // この時点で変数xはもうアクセスできない
    // let z = *x;   // エラー
}
同様に、所有権ありポインタが関数に渡されたり、フィールドに代入されたら、もうこのポインタはアクセスできなくなる。

fn bar(y: Box<int>) {
}

fn foo() {
    let x = Box::new(75);
    bar(x);
    // この時点で変数xはもうアクセスできない
    // let z = *x;    // エラー
}
Rustの所有権ありポインタはC++のstd::unique_ptrと親戚だ。C++同様、Rustでも、データを指す固有ポインタは一つしか存在できないし、ポインタがスコープ外に抜けた時点でそのデータは削除されてしまう。
だが、Rustでは、実行時チェックというよりも主に静的チェックを行っている。なので、C++で所有権移動済みのポインタへのアクセスは、実行時エラー(nullポインタ参照)になるが、Rustではコンパイルエラーになり、実行時には影響を及ぼさないのだ。

後ほど、Rustでは所有権ありポインタのデータを他種のポインタで指すことも可能だということを見ていこう。これもC++同様だが、解放済み領域へのポインタを抱えることで実行時エラーを誘発するC++に対し、Rustではそのようなことは起こりえない(具体的な方法については、Rustの他のポインタを見た際に確認しよう)。

前述した通り、所有権ありポインタは、中身を使うのにデリファレンスしなければならないところだが、メソッド呼び出しでは自動的にデリファレンスが行われる。そのため、->演算子を用意したり、*演算子でメソッド呼び出しを行う必要はない。
この点で、RustのポインタはC++のポインタと参照、両方の性質を持っていると言える。

fn bar(x: Box<foo>, y: Box<Box<Box<Box<Foo>>>>) {
    x.foo();
    y.foo();
}
Foo型がfoo()メソッドを持っているとすれば、どちらの式もOKだ。

既存のデータに対してBox::new()メソッドを呼び出すと、データの参照を実引数に渡すのではなく、データ自体をコピーする。

fn foo() {
    let x = 3;
    let mut y = Box::new(x);
    *y = 45;
    println!("x is still {}", x);
}
一般に、RustではコピーよりもMoveされる(前述の所有権ありポインタの例にもあったでしょ)ことが多い。
組み込み型は、コピーされる。したがって、上記の例で3という値はコピーされるが、より複雑な値では所有権が移動する。これについては、また後ほど掘り下げていこう。

とは言っても、プログラムを組んでいると、データを複数のポインタで指す必要が出てくるときがある。そんなときのために、Rustには所有権なしポインタが存在する。
次の投稿では、これについて見ていこう。


原文: https://github.com/nrc/r4cppp/blob/master/unique.md

0 件のコメント:

コメントを投稿

なにか意見や感想、質問などがあれば、ご自由にお書きください。