固有ポインタ
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 件のコメント:
コメントを投稿
なにか意見や感想、質問などがあれば、ご自由にお書きください。