お久しぶりです。ご無沙汰してました、はざまです。昔のハンドルネームに戻したりしましたが、ここではこのままはざまと名乗り続けると思います。
さて、本題なのですが、題名の通り、Rustプログラミング言語の公式ドキュメントであるThe book第2版の翻訳を始めました。実際には数ヶ月前から行っているので、もっと早くにご報告していればよかったですね。公式リポジトリで今後大きな変更がないと考えられるFrozen扱いされた章から順に翻訳を進めているので、牛歩ではありますが、お付き合い頂ければと思います。
ドラフトの執筆が完了するまでは、こちらのgithubページのsecond-editionディレクトリで読んでいただくしかないかと思われます。目次がなく読みにくいかとは存じますが、どうぞご容赦ください。
では、今回はここまで。どうぞ、よしなに。
主にWeb(JavaScriptやその周辺技術)とNative(ネイティブで動作するプログラミング言語。主にC++)に関するプログラミングメモや他所で公開している自作プログラムの解説、製作日記などを気ままに綴っています。
2017年10月4日水曜日
2016年3月13日日曜日
C++プログラマー向けRust 翻訳シリーズ12
クロージャと第1級関数
クロージャと第1級および、高階関数は、Rustの核心部分である。
CとC++には、関数ポインタ(とC++限定で、まったく要領のわからなかった奇妙なメンバ/メソッドポインタとかいうもの)があった。とはいうものの、比較的使われる機会は少なく、さほどプログラマーフレンドリーでもなかった。
C++11でラムダ式が導入され、こちらは、Rustのクロージャに瓜二つのものである。特に、実装方法が似通っているという点でね。
手始めに、これらの概念のさわりに触れておきたい。それから、詳細に移っていこう。
ここにfoo関数があるとしよう。定義はpub fn foo() -> u32 { 42 }だ。
さらに、別の関数barを思い浮かべよう。こちらは、引数に関数を取る(bar関数の見た目は、後述する)。宣言はfn bar(f: ...) { ... }。
foo関数をbar関数に、Cで関数ポインタを渡すような感じで与えることができる - bar(foo)。
bar関数の内部で引数fを関数かのように呼び出すことができる - let x = f();。
Rustには第1級関数が存在すると言う。理由は、関数を持ち回り、他の値同様に使うことができるからだ。
また、関数barは高階関数であると言う。理由は、関数を引数に取る、つまり、関数を操作する関数だからだ。
Rustのクロージャは、書きやすい記法の無名関数だ。|x| x + 2というクロージャは、引数を一つ取り、それに2を足して返す。なお、クロージャの引数に対して型を明示する必要はない(大抵型推論される)。また、戻り値も然りだ。
クロージャ本体が式1つ以上になる場合は、大かっこを使う - |x: i32| { let y = x + 2; y }。
クロージャも関数と同じように引数に渡せる - bar(|| 42)。
クロージャとその他の関数の大きな違いは、クロージャが周りの環境を保持することにある。これはつまり、クロージャ内からクロージャ外の変数を参照できるということである。
let x = 42; var(|| x);変数xがクロージャのスコープ内に存在するあり方に注目してほしい。
以前にもクロージャは見かけてきており、その時はイテレータとともに使用していた。これは、よくある使用方法である。具体例: ベクターの各要素に値を加える。
fn baz(v: Vec<i32>) -> Vec<i32> {
let z = 3;
v.iter().map(|x| x + z).collect()
}
ここで、引数xはクロージャに対するものであり、変数vの各要素が引数xとして渡されてくる。変数zは、クロージャの外部で定義されているが、クロージャであるがゆえに参照することができる。また、mapメソッドに関数を渡すこともできる。fn add_two(x: i32) -> i32 {
x + 2
}
fn baz(v: Vec<i32>) -> Vec<i32> {
v.iter().map(add_two).collect()
}
ちなみに、Rustでは、関数内関数も定義することができる。こちらは、クロージャではなく、つまり、環境にはアクセスできない。スコープを限るためにあるようなものである。fn qux(x: i32) {
fn quxx() -> i32 {
x // エラー: 変数xはスコープにない
}
let a = quxx();
}
関数タイプ
新しい例題関数を導入しよう。
fn add_42(x: i32) -> i64 {
x as i64 + 42
}
以前にも見かけたように、関数を変数に代入することができる。例: let a = add_42;この時、変数aの厳密な型は、Rustでは記述できない。時折、コンパイラがこれをエラーメッセージ内でfn(i32) -> i64 {add_42}と表現しているのを目撃するだろう。
各関数は、各々固有かつ匿名の型を持っている。宣言が同じ見た目でも、fn add_41(x: i32) -> i64は、異なる型になる。
いささか正確でないlet a: fn(i32) -> i64 = add_42などの型名なら、記述することができる。宣言が同じ関数はすべて、fn型に簡約化される(これなら、プログラマが記述できる)。
変数aはコンパイラに関数ポインタとして表現されるが、厳密な型を把握している場合、コンパイラはその関数ポインタを実際には使用しない。a()のような呼び出しは、aの型に基づいて静的に行われる。もし、コンパイラが(fn型であることしか把握していないなど)厳密な型を把握していない場合、呼び出しは値に含まれる関数ポインタを使用して行われる。
Fn型(先頭のFに注目)というのもあり、trait同様、制約である(実際のところ、traitである。いずれわかるだろう)。Fn(i32) -> i64はこのような宣言を持つ関数様オブジェクトの型に対する制約である。関数ポインタへの参照を取得すると、実際には、非正規化ポインタ(DSTの箇所を参照)で表されるtraitオブジェクトを生成していることになるのだ。
関数を別の関数に引き渡したり、フィールドに代入するには、型を記述しなければならない。書き方はいくつかあり、fn型ともFn型とも書くことができる。
このうち、後者の方が望ましい。なぜなら、これにはクロージャ(や可能性として他の関数様のオブジェクト)も含まれるからだ。一方、fn型には含まれない。
Fn型は動的サイズ付けである。つまり、値として使用することはできない。
関数オブジェクトを渡すか、ジェネリクスを使うかのどちらかにしなければならない。まず、ジェネリクスを使う方法を見てみよう。
fn bar<f>(f: F) -> i64
where F: Fn(i32) -> i64
{
f(0)
}
関数barは、Fn(i32) -> i64という宣言を持つ関数ならば、どんなものでも受け取ることができる(つまり、Fという型引数に対して、あらゆる関数様の型で実体化することができる)。bar(add_42)と呼び出して、関数add_42を関数barに渡せば、型引数Fをadd_42の匿名型で実体化する。また、bar(add_41)と呼び出しても動作する。
さらに、クロージャを関数barに渡すこともできる。例: bar(|x| x as i64)
これが動作するのは、クロージャの型も宣言に合致するFn型制約に紐づけられているからだ(関数のように、クロージャも各々、独自の匿名型を持っている)。
最後に、関数やクロージャへの参照を引き渡すこともできる。例: bar(&add_42)やbar(&|x| x as i64)
関数barは、fn bar(f: &Fn(i32) -> i64) ...とも書くことができる。これら2種のアプローチ法(ジェネリクスと関数/traitオブジェクト)は、全く異なる意味を持っている。
ジェネリクスの場合、関数barは単態化(造語: polymorphize - 多態化の逆から。単射化でもいいか?)され、コード生成時には、コンパイラがfの型を把握できるので、静的ディスパッチされる。
関数オブジェクトを使用しているなら、関数は単態化されない。fの厳密な型がわからないので、コンパイラは仮想ディスパッチコードを生成せねばならない。
後者の方がスピードが遅いが、前者はコード生成量が多くなる(型引数インスタンス一つにつき単態化された関数が一つ)。
実は、Fn型以外にも関数traitは存在する。FnMutとFnOnceである。使い方は、Fn型と同じである。例: FnOnce(i32) -> i64
FnMut型は、呼び出し中に可変化できるオブジェクトを表す。これは、普通の関数には適用されず、クロージャに作用してクロージャが環境を可変化できるようにする。
FnOnceは、(最大でも)1回しか呼び出せない関数を表し、こちらもまたクロージャにしか関連しない。
Fn型とFnMut型、FnOnce型は、継承関係にある。Fn型はFnMut型でもあり(Fn型の関数を可変化許可を得た状態で呼び出しても何ら害はないが、逆は言えない)、Fn型とFnMut型は、FnOnce型でもある(通常の関数を1回しか呼び出さなくても害はないが、逆は言えない)。
以上より、高階関数をなるべく柔軟にするには、Fn型ではなく、FnOnce型を使うべきだ(あるいは、この関数を2回以上呼び出す必然性があるなら、FnMut型を使う)。
メソッド
メソッドは、関数と全く同じように使用できる。つまり、ポインタ化したり、変数に代入するなど。ドット演算子は使用できず、メソッド名をフルネーム(UFCS - universal function call syntax ~普遍的関数呼び出し記法~と呼ばれることもある)で記述しなければならない。
self引数がメソッドの第一引数になる。
struct Foo;
impl Foo {
fn bar(&self) {}
}
trait T {
fn baz(&self);
}
impl T for Foo {
fn baz(&self) {}
}
fn main() {
// 固有メソッド
let x = Foo::bar;
x(&Foo);
// traitメソッド。フルネームで記述していることに注目
let y = <foo as T>::baz;
y(&Foo);
}
汎用メソッド
汎用メソッドへのポインタを取得することはできず、汎用関数型を表現する手段も存在しない。しかし、全型引数がインスタンス化されていれば、関数への参照を取ることができる。
fn foo<T>(x: &T) {}
fn main() {
let x = &foo::<i32>;
x(&42);
}
汎用クロージャを定義する方法もない。複数の型に作用するクロージャが必要ならば、traitオブジェクトやマクロ(でクロージャを生成すること)を使ったり、クロージャを返すクロージャ(返ってくるクロージャごとに違う型に作用する)を渡せばいい。汎用ライフタイム関数と超高位型(higher-ranked type)
ライフタイムについて汎用的な関数型やクロージャを存在させることができる。
無所有権参照を取るクロージャを想像してほしい。参照のライフタイムが何であれ、このクロージャは同じ挙動をするが(また、実際のところ、コンパイル済みのコードからは、ライフタイムは消去される)、その型定義はどんな感じだろうか?
fn foo<f>(x: &Bar, f: F) -> &Baz
where F: Fn(&Bar) -> &Baz
{
f(x)
}
この時、参照のライフタイムは何になるだろうか?この単純な例では、単一ライフタイムを使用しても構わない(汎用クロージャを使う必要性はない)。fn foo<'b, F>(x: &'b Bar, f: F) -> &'b Baz
where F: Fn(&'b Bar) -> &'b Baz
{
f(x)
}
しかし、変数fに異なるライフタイムを入力できるようにする必要があったらどうだろうか?その場合は、汎用的な関数型が必要になる。fn foo<'b, 'c, F>(x: &'b Bar, y: &'c Bar, f: F) -> (&'b Baz, &'c Baz)
where F: for<'a> Fn(&'a Bar) -> &'a Baz
{
(f(x), f(y))
}
ここでの新規要素は、for<'a>という箇所であり、これは、ライフタイムについて汎用的な関数型を記述し、「すべての'a, ...に対して」と読む。専門用語で言えば、この関数型は普遍定量化されているという。なお、上述の例で'aをfooに捕らえさせることはできない。反例:
fn foo<'a, 'b, 'c, F>(x: &'b Bar, y: &'c Bar, f: F) -> (&'b Baz, &'c Baz)
where F: Fn(&'a Bar) -> &'a Baz
{
(f(x), f(y))
}
これはコンパイルが通らない。なぜなら、コンパイラがfooの呼び出しに対してライフタイムを推論する際、'aに対して単一のライフタイムを選択しなければならないが、'bと'cが異なる場合にはそれができないからである。このような感じで汎用的な関数型は、超高位型(higher-ranked type)と呼ばれる。上層のスコープのライフタイム変数は、ランク1になる。上記の例の'aは、上位スコープに移動できないため、このランクは2以上ということになる。
超高位関数型の引数を持つ関数を呼び出すのは簡単だ。コンパイラがライフタイム引数を推論してくれる。例: foo(&Bar { ... }, &Bar {...}, |b| &b.field)
実際問題、たいていの場合、そのようなことを気にかける必要さえない。関数引数のライフタイムを省略できるのと同じようにして、コンパイラが定量化されたライフタイムを省略させてくれる。具体的には、上記の例を以下のように書き換えることができる。
fn foo<'b, 'c, F>(x: &'b Bar, y: &'c Bar, f: F) -> (&'b Baz, &'c Baz)
where F: Fn(&Bar) -> &Baz
{
(f(x), f(y))
}
(これは不適切な例なので、'bと'cしかライフタイムパラメータは必要ない)Rustにおいて、無所有権参照を含む関数型が認識される箇所には、通常の省略ルールが適用され、この関数型(すなわち、超高位型)のスコープにおいて省略された変数の定量化が行われる。
このような非常に稀な使用例に対して、なぜこんなに悩まされなければならないのかと疑問に思っているかもしれないね。本当のきっかけは、外部の関数から渡されるデータに処理を施す関数を引数にする関数なのだ。
fn foo<f>(f: F)
where F: Fn(&i32) // 完全明示記法: for<'a> Fn(&'a i32)
{
let data = 42;
f(&data)
}
このようなケースの場合は、超高位型が必要不可欠になる。代替手段として関数fooにライフタイム引数を追加したとしても、正常なライフタイムを推論することはできない。その理由を探るために、どのような挙動をするのか見てみよう。fn foo<'a, F: Fn(&'a i32')> ...というものを考えてほしい。Rustでは、いかなるライフタイム引数も、自分が定義されている文法項目より長生きすることが必須条件になる(このような条件がない場合、このライフタイムを持つ実引数が、その関数内で使用できるが、ここでの存在は保証されないことになってしまう)。
foo関数内で、f(&data)という記述をしており、この参照のライフタイムはコンパイラにより推論され、(最大でも)変数dataが定義された箇所から、この変数がスコープを抜けるまでの間存在する。ライフタイム変数'aは、関数fooよりも長生きせねばならないが、ここで推論されたライフタイムはそうはならないため、このような方法で関数様オブジェクトfを呼び出すことはできない。
ただ、超高位型ライフタイムがあれば、オブジェクトfはどんなライフタイムでも受け取れることになり、&dataの無名ライフタイムも道理が通るので、この関数型もチェックが付くのだ。
Enumコンストラクタ
少し関係のない話をするが、役に立つこともある豆知識を紹介しょう。enumの状態はすべて、その状態のフィールドからenum自体にマッピングする関数を定義している。
enum Foo {
Bar,
Baz(i32),
}
これは二つ関数を定義する。Foo::Bar: Fn() -> FooとFoo::Baz: Fn(i32) -> Fooというものだ。通常、各状態をこのように使うことはない。状態は関数というよりも、データ型として扱われるのだ。しかし、時として役に立つこともある。具体的には、i32型のリストがあるとして、以下のようにしてenum Fooのリストを作れたりすることだ。list_of_i32.iter().map(Foo::Baz).collect()
クロージャの風味付け
クロージャは2種類の入力を持つ。明示的に引き渡される実引数と、環境から横取りする変数だ。普段なら、いずれの入力も推論されるので、(訳注: 特に気にかける必要はないが)、必要に応じて細かい制御を行うこともできる。
実引数に関しては、コンパイラに型推論させるのではなく、型を宣言することができる。これは戻り値の型にも適用できる。
|x| { ... }と書く代わりに|x: i32| -> String { ... }と書けばいい。実引数が所有権ありになるか、所有権なしになるかは、(宣言されていようが、型推論だろうが)型によって決まる。
捕捉される変数については、ほとんどの場合、型はその環境からわかっているが、もう少し魔法の呪文があるのだ。捕捉変数が参照渡しになるか、値渡しになるか?
コンパイラは、クロージャ本体からこの情報を推論し、可能な限り、参照渡しをしてくれる。
fn foo(x: Bar) {
let f = || { ... x ... };
}
すべてがつつがなくいっていれば、クロージャf内で、引数xは関数fooの生存期間を持つ&Bar型になる。ところが、引数xが可変な場合、捕捉変数は可変参照渡しと推論され、すなわち引数xの型は&mut Barになる。引数xがクロージャf内でムーブ(変数や値型のフィールドに代入されるなど)されていると、捕捉変数は値渡しと推論され、すなわち型はBar型となる。
この挙動は、プログラマーが変更できる(クロージャがフィールドに代入されたり、戻り値になる場合には必要になることもある)。クロージャの前にmoveキーワードを付ければ、捕捉変数はすべて値渡しされるようになる。具体例: let f = move || { ... x ... };と書くと、引数xの型は常にBar型になる。
先刻、関数の種類について話をした。つまり、Fn、FnMut、FnOnceの話だ。今なら、なぜこれらが必要なのか説明がつく。
クロージャにおいて、可変性と呼び出しの唯一性は、捕捉変数にかかるものである。捕捉によって、捕捉された変数が一つでも可変になったら、FnMut型になる(なお、コンパイラにより推論されるものなので、宣言は必要ない)。
変数がクロージャにムーブされていたら、つまり、値渡しになっていたら(moveの明示と型推論によるもの、両方の可能性がある)、クロージャはFnOnce型になる。このようなクロージャを二度以上呼び出してしまうと、複数回捕捉変数がムーブされることになるので、危険である。
コンパイラは可能な限り、クロージャが柔軟な型になるよう、推論を行う。
実装
クロージャは、無名構造体として実装されている。この構造体が、クロージャの捕捉変数を各々フィールドとして格納しているのだ。この構造体は、単独のライフタイム引数を持ち、これが捕捉変数のライフタイムに制約としてかかる。また、この無名構造体はcallという名のメソッドを持ち、これを使ってクロージャを実行する。
fn main() {
let x = Foo { ... };
let f = |y| x.get_number() + y;
let z = f(42);
}
例として、上記のクロージャを考えよう。これをコンパイラは、以下のように解釈する。struct Closure14<'env> {
x: &'env Foo,
}
// 実際の実装とは異なる。以下を参照
impl<'env> Closure14<'env> {
fn call(&self, y: i32) -> i32 {
self.x.get_number() + y
}
}
fn main() {
let x = Foo { ... };
let f = Closure14 { x: x }
let z = f.call(42);
}
前述したように、3つの異なる関数traitが存在する。FnとFnMut、FnOnceだ。現実的には、callメソッドはこれらのtraitが必要とするものであって、実装に固有であるものではない。Fn型のcallメソッドは、特殊変数selfを参照で取り、FnMut型のcall_mutメソッドは可変参照で、FnOnce型のcall_onceメソッドは特殊変数selfを値で取る。
ここまで見てきた関数型は、Fn(i32) -> i32のような形をしており、あまりtrait型らしくない。ここには、少し秘密の呪文がかかっているのだ。コンパイラは、この丸括弧形砂糖を関数型にしか、かけさせてくれない。通常の型(山括弧型)に精製すると、引数の型がタプルとして扱われ、型パラメータで渡され、戻り値型もOutputと呼ばれる結合型になる。
以上より、Fn(i32) -> i32は、Fn<(i32,), Output=i32>という型に精製されるので、Fn traitの定義は以下のようになる。
pub trait Fn<args> : FnMut<args> {
fn call(&self, args: Args) -> Self.Output;
}
したがって、上記のClosure14型の実装はむしろこうなる。impl<'env> FnOnce<(i32,)> for Closure14<'env> {
type Output = i32;
fn call_once(self, args: (i32,)) -> i32 {
...
}
}
impl<'env> FnMut<(i32,)> for Closure14<'env> {
fn call_mut(&mut self, args: (i32,)) -> i32 {
...
}
}
impl<'env> Fn<(i32,)> for Closure14<'env> {
fn call(&self, args: (i32,)) -> i32 {
...
}
}
この関数traitは、core::opsモジュール内に存在する。先ほど、ジェネリクスを使用すると、静的ディスパッチになり、traitオブジェクトを使用すると、仮想ディスパッチになる話をした。今なら、もう少しその理由について突っ込んで話すことができる。
callメソッドの呼び出しは、静的メソッドディスパッチになり、仮想ディスパッチにはならない。単態化された関数を渡しても、やはり型は静的に把握できるため、静的ディスパッチになる。
クロージャをtraitオブジェクトに押し込むことができる。具体例: &fやBox::new(f)で型は&Fn(i32)->i32とBox<Fn(i32)->i32>になる。
これらは、ポインタ型になり、traitを指しているので、非正規化ポインタである。要するに、データ自体を指すポインタとvtable(翻訳者注: C++などで使われる仮想メソッドルックアップテーブルのこと。これとメタデータを用いて、実際に呼び出すメソッドの実装を決定する)を指すポインタで構成されるということだ。vtableを使用して、callメソッド(やcall_mutメソッドなど何でも)のアドレスを参照する。
時として、これら2種のクロージャの表現方法を箱詰め型クロージャや非箱詰め型クロージャなどと呼んでいるのを見かけるだろう。非箱詰め型クロージャは、静的ディスパッチによる値渡し型、箱詰め型クロージャは、動的ディスパッチによるtraitオブジェクト型のものをいう。
かつてRustには、箱詰め型のクロージャしか存在しなかった(また、システムも全く異なっていた)。
参考資料
注: 以下の資料はすべて、英語表記
- RFC114-Closures
- Finding Closure in Rust blog post
- RFC387-Higher ranked trait bounds
- Purging proc blog post
翻訳者後記: お疲れ様でした。これにて、Rust for C++ programmersブログポストシリーズの翻訳は終わりです。原文にはTODOが記載されていて、今後加筆修正がありそうなため、適宜修正は行うつもりですが、原文とリンクしていなくても怒らないでください。
この記事を通して、一人でも多くのC++プログラマーがRustの利便性や動作原理などを理解していただけたら、翻訳者冥利に尽きます。
なお末筆ながら、この記事により発生した損害、賠償などの責務は当方では負いかねます。予めご了承ください。(訳: この記事の訳語を使用したけど、周りのプログラマーに通じなかったから責任取れなど)
原文: https://github.com/nrc/r4cppp/blob/master/closures.md
原文: https://github.com/nrc/r4cppp/blob/master/closures.md
2016年3月11日金曜日
C++プログラマー向けRust 翻訳シリーズ11
グラフとアリーナ型メモリアロケータ
(注: この章の例は、このディレクトリをダウンロードしてcargo runコマンドを入力すると、実際に動かすことができる)
グラフ構造は、Rustにおいて非常に厳密なライフタイム管理と可変性管理があるため、いささか構築がめんどくさい。ただオブジェクト指向プログラミングにおいて、オブジェクトのグラフ構造は、非常によく使われるものである。
このチュートリアルでは、グラフ構造の実装方法を数種類提示していこうと思う。個人的には、アリーナ型メモリアロケータを使用し、多少ライフタイムの明示を駆使する方法が好みだ。また最後に、今後Rustに導入されそうな機能のうち、ここで紹介する実装方法の簡略化を行ってくれそうなものについて議論して締めたいと思う。
グラフ構造は、一連のノードとそのノード間を結ぶエッジで構成され、リストや木構造を一般化したものと言える。各ノードは、複数の子や親を持つことがある(尤も、グラフ理論では親子とは呼ばずに、内向・外向という)。
グラフ構造は、隣接するリストや行列で表すことができる。前者は、根本的にはグラフのノードを表すオブジェクトがあり、そのオブジェクトが隣接するノードのリストを持つものである。一方、行列で表す場合は、行ノードから列ノードへの辺があるかどうかを表す論理値の行列を使う。
ここでは、隣接リスト方式での実装方法のみ解説する。隣接行列方式は、Rust固有とは言い難い全く別の問題を抱えているのだ。
本質的には、二種の直交する問題が存在する。どうやってグラフ全体のライフタイムを管理するかと、どうやってその可変性を管理するかだ。
一つ目の問題は、究極的には、グラフ内の他のノードを指し示すのにどんなポインタを使うかという問題に帰着する。グラフ様のデータ構造は、再帰的であるため(たとえ、データはそうでなくとも、型自体が再帰的になる)、完全に値だけでグラフ構造を構築することはできず、何らかのポインタを使わざるをえなくなる。
グラフ構造は再帰的になりうるが、Rustの所有権は再帰的にはできないため、Box<Node>型を使用することはできない(ただ、擬似木構造のデータや連結リストには使用できるけど)。
いかなるグラフも真の意味で、不変たりえない。どこかで円環状になる可能性があるため、一文でグラフを構築することはできないからだ。ゆえに、最低限でも、初期化処理中はグラフを可変にする必要がある。
Rustにおいて、通常、ポインタはすべて固有か不変でなければならないというのは、普遍の真理である。グラフの辺は、(少なくとも初期化中は)可変でなければならず、いかなるノードにも内向する辺が2つ以上存在する可能性がある以上、辺の固有性を保証することはできない。したがって、何かしら幾分高度なことをして、可変性を扱わねばならないわけだ。
解決策の一つとして、可変生ポインタ(*mut Node)を使うことが挙げられる。これは、最も柔軟性の高い手段だが、同時に最も危険でもある。
ライフタイム管理は、型システムのサポートなしにすべて自分で行わなければならない。こうすれば、非常に柔軟で効率的なデータ構造を構築できるが、同時に慎重にならねばならない。
この方法ならば、先ほどのライフタイムと可変性問題は一掃できる。しかし、それは結局、Rustの利点をすべて無視することで得られるものである。つまり、コンパイラの支援を受けることはできない(また、生ポインタは、自動(被)参照しないため、特別人間に優しくない)。生ポインタを使ったグラフ構造は、C++のものと大して相違ないので、ここでは解説しない。
ライフタイム管理には参照カウント(共有所有権、Rc<...>を使用)かアリーナ型メモリアロケータ(全ノードがアリーナに管理された同じライフタイムを持つ、無所有権参照&...を使用)を使用するという選択肢がある。前者の方がより柔軟(あらゆるライフタイムを持つ個々のノードを外部から参照することができる)であるが、後者の方はそれ以外のあらゆる点で勝っている。
可変性管理について、RefCellクラスを使用してRustの動的な内部可変性機構を援用するか、可変性を自ら管理することができる(この場合、UnsafeCellクラスを使用して内部可変性をコンパイラとやりとりせねばならない)。前者の方が安全、後者はより効率的、両者ともプログラマーフレンドリーではない。
なお、円環状になっている可能性のあるグラフがあるのに、Rcクラスを使用している場合、さらなるアクションを行って再帰構造を破り、メモリリークを避ける必要がある。Rustには、Rcポインタを再帰的に保持できるコレクションがないため、グラフ内に再帰構造がある場合、参照カウントが0にならず、グラフは解放されなくなる。
グラフ内にWeakポインタを使用するか、グラフ破棄の時期に再帰構造を手動で破ることで解決できる。前者の方が信頼性が高い。ここでは、どちらも解説しない。例では、単にメモリリークさせる。
無所有権参照とアリーナ型メモリアロケータを使用したアプローチならば、このような問題はないため、その点で優れている。
アプローチの比較のため、例は非常にシンプルに保つ。グラフ内の一ノードを表すNodeオブジェクトに、文字列データ(より複雑なデータの代わり)と隣接ノード(edges)のVecを持たせる。複数ノードを持つシンプルなグラフを作成するinit関数と事前順序付け深さ優先探索を行うtraverse関数を定義する。traverse関数で各ノードのデータを表示する。
最後に、selfが表すノードの最初に隣接したノードへの参照を返すNode::firstメソッドとノードの持つデータを表示するfoo関数を定義する。これらの関数を介してグラフ内部のより複雑なノード操作を行う。
平易になりすぎずになるべく情報量を詰め込めるよう、可能な組み合わせのうち2通りを解説する。参照カウントとRefCellクラスを使用するものと、アリーナ型メモリアロケータとUnsafeCellクラスを使用するものだ。他の2通りの組み合わせは、学習用に残しておく。
Rc<RefCell<Node>>
完全な例はこちら。
unsafeコードがないので、こちらの方が安全な選択肢である。また、最も非効率的で人間工学的でもない。ただ、非常に柔軟ではある。ノードが参照カウントされているため、グラフ外での再利用も利くからね。完全に可変なグラフが必要だったり、グラフ自体の存在にかかわらず、ノードが必要な場合は、こちらの手段を取るといいだろう。
ノードは以下のような構造をしている。
struct Node {
datum: &'static str,
edges: Vec<Rc<RefCell<Node>>>,
}
新規ノードを作るのは、さほどめんどくさくない。Rc::new(RefCell::new(Node { ... }))と書けばいい。初期化時に辺を追加するには、先頭ノードを可変無所有権参照し、終端ノードをクローン(こうすると、ポインタをコピーし、参照カウントを1増やす。ノード自体はコピーしない)して辺のベクターコンテナに追加する。let mut mut_root = root.borrow_mut(); mut_root.edges.push(b.clone());RefCellクラスを使うことで、書き込み時にノードが読み書き中ではないことが動的に保証される。
ノードへのアクセスは、必ず.borrow()メソッドを使用してRefCellクラスを無所有権参照しなければならない。firstメソッドは、無所有権参照ではなく、参照カウント式ポインタを返す必要がある。従って、firstメソッドの呼び出し元でも、無所有権参照せねばならない。
fn first(&self) -> Rc<RefCell<Node>> {
self.edges[0].clone()
}
pub fn main() {
let g = ...;
let f = g.first();
foo(&*f.borrow());
}
&NodeとUnsafeCell
完全な例はこちら。
このアプローチでは、辺に無所有権参照を用いる。これは素晴らしく人間工学的だ。こうすれば、主に無所有権参照(なお、Rustの参照カウント式オブジェクトの素晴らしい点は、ライフタイムシステムによく馴染むことにある。Rcクラスへの無所有権参照を作成して、直接かつ安全にデータを参照することができる。先ほどの例において、RefCellクラスのせいでこのようなことはできなかったが、RcかUnsafeCellクラスならば可能なはずだ)を対象とするRustの標準的なライブラリを、ノードに対して援用することができるようになるのだ。
また、破棄も正常に行われる。唯一の制約は、全ノードが同時に破棄されなければいけないことだけである。ノードの破棄および、確保は、アリーナで行われる。
他方、わずかではあるが、ライフタイムを明示しなければいけない。残念ながら、ここでライフタイム省略記法の恩恵にあずかることはできない。この節の末尾で、このような事態を改善する言語の進化の方向性を探っていく。
構築フェーズでは、複数参照される可能性のあるノードを可変化するだろう。このようなことは、通常のRustコード内では不可能なので、unsafeブロック内で初期化しなければならない。ノードが可変かつ複数参照されているので、UnsafeCellクラスを使用して通常の不変性原則に依存しないことを、Rustコンパイラに通知するのだ。
では、いつこのアプローチは現実的になるのだろうか?
グラフは、初期化時のみ可変である必要がある。加えて、グラフ内の全ノードは同じライフタイムである必要がある(同時に破棄することが可能であれば、この制約を緩めて後々ノードを追加するようにもできる)。
同様に、ノードを可変化できる時期について、より複雑な不変性原則に依存することもできるが、実装を単純には保てなくなる。プログラマーがそれらの安全について面倒を見なければいけなくなるからね。
アリーナ型メモリアロケーションは、メモリ管理法の一種であり、一まとまりのオブジェクトが同じライフタイムを持ち、同時に解放される。
アリーナとは、メモリ確保と解放を司るオブジェクトである。(個々のオブジェクトごとにメモリ確保するのではなく)ひとくくりに巨大なメモリ領域が確保されたり、解放されたりするため、アリーナのメモリ確保は非常に効率的だ。
通常、オブジェクトはすべて連続したメモリ領域に確保される。そうすれば、グラフを辿る際のキャッシュ利用効率が上がる。
Rustにおいて、アリーナ型メモリアロケーションは、libarenaでサポートされ、コンパイラ全体で活用されている。アリーナには二種類ある。型付けアリーナと型なしアリーナだ。
前者の方が、効率的で使いやすいが、ある型のオブジェクトしか確保できない一方で、後者ならば、より柔軟でどんなオブジェクトでも確保することができる。
アリーナで確保されたオブジェクトは、すべて同じライフタイムであり、このライフタイムはアリーナオブジェクトのパラメータになる。型システムにより、アリーナにより確保されたオブジェクトへの参照が、アリーナ自体よりも長く生き残り続けないことを保証してくれる。
さて、ノード構造体にグラフのライフタイム('a)を含める必要が出た。隣接ノードのVecコンテナをUnsafeCellクラスで包んで、不変であるはずのVecコンテナを可変化できるようにする。
struct Node<'a> {
datum: &'static str,
edges: UnsafeCell<Vec<&'a Node<'a>>>,
}
new関数もこのライフタイムを使用し、メモリ確保を行うアリーナオブジェクトを引数に取らなければならない。fn new<'a>(datum: &'static str, arena: &'a TypedArena<Node<'a>>) -> &'a Node<'a> {
arena.alloc(Node {
datum: datum,
edges: UnsafeCell::new(Vec::new()),
})
}
アリーナオブジェクトを使ってノードオブジェクトのメモリ確保を行っている。グラフのライフタイムは、アリーナオブジェクトへの参照から派生している。そのため、アリーナオブジェクトは、グラフのライフタイムを包括するスコープから渡す必要がある。今回の例で言えば、initメソッドに渡すことを意味する。(表記上のスコープ外で値を生成できるように型システムを拡張することが考えられるが、今すぐ実装の予定はない)。アリーナオブジェクトがスコープ外に抜ければ、グラフ全体も破棄される(Rustの型システムにより、この時点以降までグラフへの参照が残らないことが保証される)。
辺の追加方法は少し趣が異なる。
(*root.edges.get()).push(b);本質的には、root.edges.push(b)を呼び出してノード(b)を辺のリストに追加しているだけである。とはいえ、edgesフィールドがUnsafeCellクラスに包まれているため、get()を呼んであげる必要がある。こうすると、辺への可変生ポインタ(*mut Vec<&Node>)が得られ、edgesフィールドを可変化することができる。ところが、これはこのポインタを手動で被参照しなければいけないこと(生ポインタは自動被参照できない)でもあり、(*...)と書いているのだ。
最後に、生ポインタの被参照は非安全なので、全体をunsafeブロックに入れ込む必要がある。
traverse関数の気になる箇所は以下の通りだ。
for n in &(*self.edges.get()) {
n.traverse(f, seen);
}
辺のリストを取得するのに先ほどと同じ手順を踏んでいるため、unsafeブロックが必要になる。この場合、実際には安全である。なぜなら、すでに初期化は完了しており、可変化処理がないからだ。さて、firstメソッドも辺リストを取得する手順は同じであり、unsafeブロックが必要になる。ただ、Rc<RefCell<_>>を使用したグラフとは対照的に、ノードへの単純な無所有権参照を返せばいい。とても便利だ。どこにも可変化処理がなく、初期化済みのため、このunsafeブロックは安全だと推論できる。
fn first(&'a self) -> &'a Node<'a> {
unsafe {
(*self.edges.get())[0]
}
}
このアプローチに対する将来的な言語の改善点
Rustにおいて、アリーナ型メモリアロケーションと無所有権参照の使用は、重要なパターンと考えられる。言語に改良を施して、これらのパターンをより安全に使いやすくするべきだ。アリーナの使用がメモリアロケータに対する現在進行中の改良によって、よりプログラマーフレンドリーになることを願っている。
その他、著者が把握している改良点は3つある。
安全な初期化処理
オブジェクト指向の世界では、初期化時のみ可変性を保つ機構に関して多数の調査が行われてきた。一体、この機構がRustでいかように作動するかは、未解決な疑問だが、可変ではあるが固有ではなく、スコープに閉じられたポインタを示す必要があるということのようだ。そのスコープ外では、既存のポインタはいかなるものであっても、通常の無所有権参照、つまり、不変か、固有になるのだ。
そのような機構の利点は、初期化時は可変で、それから不変になる一般的なパターンを示す手段ができることにある。また、これは、個々のオブジェクト自体は、複数所有されているものの、その集合全体(今は、グラフ)は単一所有されているという普遍的原則にも依存している。
こうすれば、UnsafeCellクラスとunsafeブロックの必要なく、参照とUnsafeCellクラスを使用するアプローチを適用することができるようになり、この手段がよりプログラマーフレンドリーかつ安全なものになる。
ETH Zurich(訳注: 大学名か何か?)のAlex SummersとJulian Viereckが、これを追求している。
汎用的なモジュール
グラフのライフタイムは、いかなるグラフでも定数になる。ライフタイムを繰り返し記述するのは、冗長でしかない。
これをプログラマーフレンドリーにする一手段は、グラフのモジュールにライフタイムのパラメータを持たせられるようにすることだ。そうすれば、struct、impl、関数ごとに記述する必要がなくなる。
それでも、グラフのライフタイムは、モジュール外から指定する必要があるが、たいていの場合、推論でなんとかなる(今日、関数呼び出しではできている)と好ましい。
モジュールがどんな見た目になるかは、ref_graph_generic_mod.rsを参照されたし。((上記で確約されている)安全初期化処理を援用してunsafeコードを排除することもできるはずだ)
また、このRFC issueも参照されたし。
この機能によって、参照とUnsafeCellクラスアプローチのコードの重複箇所が大幅に削減されるだろう。
ライフタイム省略
現時点で、プログラマーは関数定義のライフタイムを一部省略してプログラマーフレンドリーにすることができる。グラフに対して&Node型を使用するアプローチが、いささか見目麗しくない理由の一つは、ライフタイム省略記法を使用していないからだ。
Rustにおいてよく使われるイディオムに共通ライフタイムを持つデータ構造がある。そのようなデータ構造への参照は、&'a Foo<'a>のような型を立てる。具体例: グラフの例では&'a Node<'a>。
このようなケースに役に立つ省略記法があると便利だが、どんな挙動をすべきかは確信が持てない。
汎用モジュールの例を見ると、さほどライフタイム省略記法を拡張する必要はないようだ(Node::newメソッドがライフタイムを与えられなくても動作するか皆目見当がつかないが、仮にそうであっても、ほんの些細な拡張を施すだけで、動作するようになるだろう)。
スコープ内で唯一のときは、モジュール全体で('static以外の)ライフタイムを省略できる何らかの新しいルールを追加したくなるかもしれないが、スコープ内に複数のライフタイムがある場合(fooとinit関数を参照)の挙動が読めない。
汎用モジュールを追加しなくても、&'a Node<'a>という書き方に特化した省略記法を導入する可能性はある。まあ、どう実装するのかは知ったこっちゃないけど。
原文: https://github.com/nrc/r4cppp/blob/master/graphs/README.md
2016年3月10日木曜日
C++プログラマー向けRust 翻訳シリーズ10
配列およびVectorコンテナ
Rustの配列は、Cの配列とは全く別物だ。簡単に言うと、固定長配列と可変長配列の両側面を持っているのだ。これらは一般的には、固定長配列と配列スライスとして知られている。
いずれわかることだが、前者の名前はどこか的確でない。だってどちらの配列も固定長(可変長の対義語という意味)だからね。可変長配列カテゴリとしてRustでは、Vecというコレクションクラスが用意されている。
固定長配列
固定長配列の配列長は、コンパイル時に決定され、一つの型と考えられる。例: [i32; 4]は配列長4のi32型の配列。
配列のリテラル表記やアクセス方法は、Cと変わらない。
let a: [i32; 4] = [1, 2, 3, 4]; // もちろん、型注記は必須ではない
println!("二番目の要素は{}", a[1]);
配列のインデックスがC同様、0起点のことに気づいたかな。ところが、CやC++と異なり、インデックスは境界値チェックが行われる。事実、配列へのアクセスはすべて境界値チェックが行われる。これもRustがより安全な言語と言える根拠の一つだ。
ここでa[4]と書こうとすると、ランタイムエラーが発生する。残念ながら、Rustのコンパイラはコンパイルエラーを出してくれるほど賢明ではないのだ。上記の例みたいにエラーになるのが一目瞭然であってもね。
あなたが危ない橋を渡りたがりだったり、プログラムのパフォーマンスを極限まで追求しなきゃいけない場合、境界値チェックを行わない方法もある。配列に対して、get_uncheckedメソッドを呼び出せばいい。境界値非チェックは、unsafeブロックで行う必要がある。こんなことをする機会は極々少ないはずだ。
他のデータ構造同様、Rustにおいて、配列は規定で不変になり、可変性は伝播する。また、インデックスアクセスでも可変化できる。
let mut a = [1, 2, 3, 4];
a[3] = 5;
println!("{:?}", a);
さらに、参照を取ることで配列を無所有権参照することもできる。fn foo(a: &[i32; 4]) {
println!("先頭: {}; 最後: {}", a[0], a[3]);
}
fn main() {
foo(&[1, 2, 3, 4]);
}
無所有権参照された配列でも、インデックスアクセスできることに注目してほしい。ここら辺で、C++プログラマーにとってRustの配列の気になりやすい側面について語っておこう。メモリ構造だ。
Rustの配列は、値型である。つまり、配列は他の値同様、スタック上に確保され、配列オブジェクトは(Cで見られるような)値へのポインタではなく、データ列で表現される。
ゆえに前述の例で、let a = [1_i32, 2, 3, 4];は、スタック上に16バイトのメモリ領域を確保し、let b = a;と書けば、さらにこの16バイトの領域をコピーする。Cのような配列が必要なら、明示的に配列へのポインタを作成する必要がある。そうすれば、先頭要素へのポインタが得られる。
最後に、Rustの配列はtraitを実装できる。ゆえに、メソッドを持っている。
なので、配列長を得るには、a.len()を呼び出せばいい。
配列スライス
Rustにおける配列スライスは、ただ単に配列長がコンパイル時に決定していないだけの配列である。型の表記法も、固定長配列と同じであるが、長さを明示する必要はない。具体例: [i32]は32ビット整数の配列スライス(配列長は動的に決まる)だ。
ただ、配列スライスには落とし穴がある。Rustにおいて、コンパイラは全オブジェクトのサイズを把握しておく必要があり、配列スライスのサイズは把握できないから、配列スライスを値で保持することはできないのだ。fn foo(x: [i32])などと書こうものなら、たちまちコンパイラがエラーを吐くだろう。
そこで、配列スライスは常にポインタで保持しなければならない(このルールには、オレオレスマポを実装するという非常に高度な技術を要する例外があるが、今は無視しても構わない)。要約すると、(配列スライスを無所有権参照するなら)fn foo(x: &[i32])、(配列スライスに対して可変生ポインタを得るなら)fn foo(x: *mut [i32])などと書かねばならない。
配列スライスを生成する最も単純な方法は、簡略化を使用することだ。
C++と比較して、Rustの暗黙的簡略化は、はるかに少ない。そのうちの一つが、固定長配列から配列スライスへの簡略化である。配列スライスはポインタでなければならないから、これは実質的にポインタ間の簡略化になる。
例えば、&[i32; 4]から&[i32]への簡略化は以下のようにして行える。
let a: &[i32] = &[1, 2, 3, 4];ここで、右辺はスタック上に確保された配列長4の固定長配列である。そこから参照(型は&[i32; 4])を取り、この参照が&[i32]型に簡略化され、let文によりaという名前を与えられている。
配列スライスもまた、C同様に([...]で)アクセスし、境界値チェックが行われる。len()メソッドを呼び出せば、手動で配列長を確認することもできる。したがって、いずれかの時点で配列長が決定するのは明白だ。
実際、Rustの配列はどんな種類でも、配列長が決まっており、これが境界値チェックの肝であるため、メモリ安全性に貢献する要素たるのだ。
サイズは(固定長配列の静的決定と対称的に)動的に定まるため、配列スライスは動的サイズ付けタイプ(英語略称: DST - Dynamically Sized Types. 他にも動的にサイズが決定する型があるので、別の機会に解説する)と呼ばれる。
配列スライスオブジェクトは、ただのデータ列でしかないため、オブジェクト内にサイズは格納できない。代わりに、サイズはポインタの一部になる(配列スライスは、絶対にポインタでなければならないことを覚えているだろうか)。
(他の動的サイズ付けタイプへのポインタ同様)配列スライスへのポインタは、非正規化ポインタ(訳注: 造語; fat pointer - regular pointerが1ワード長に対して、それよりもサイズが大きいので)になる。非正規化ポインタとは、1ワード長ではなく、2ワード長で、データを指すポインタと追加のデータを持つものである。配列スライスの場合、追加データは、スライスの長さになる。
以上より、上記の例でポインタaは(64ビットシステム上では)128ビット長になり、前半部分にデータ列の先頭となる1へのメモリアドレスが、後半には長さの4が格納される。
Rustのプログラマー的に普段は、これら非正規化ポインタも通常のポインタと同等に扱って構わないが、少しでもかじっておくのはいいことだ(具体的には、キャストでできることなどに影響してくる)。
スライス記法と範囲オブジェクト
配列スライスは、配列に対する(無所有権参照の)窓と捉えることができる。ここまで、配列全体のスライスしか見てこなかったが、それ以外にも配列の一部をスライス化することもできる。これには、インデックス表記に似た特殊な書き方があり、整数を一つ取る代わりに範囲オブジェクトを与えることで作用させる。具体例: a[0..4]は配列aの先頭4要素をスライス化する。
範囲オブジェクトは、上限は含まず、下限は含むことに注意してほしい。
let a: [i32; 4] = [1, 2, 3, 4]; let b: &[i32] = &a; // 配列全体をスライス化 let c = &a[0..4]; // bの別表記。型も&[i32] let c = &a[1..3]; // 中間2要素。型は&[i32] let c = &a[1..]; // 末尾3要素 let c = &a[..3]; // 先頭3要素 let c = &a[..]; // またまたbの別表記 let c = &b[1..3]; // 配列スライスもスライス化できる最後の例において、スライスに対しても、無所有権参照をする必要があることに注目してほしい。スライス記法は、スライスオブジェクト(型は[i32])を生成する。なので、無所有権参照されたスライスオブジェクトをスライス化していても、これを無所有権参照(そうすれば&[i32]型のオブジェクトが得られる)しなければならない。
範囲記法は、スライス記法以外でも使用できる。a..bという書き方は、aからb-1までを列挙するイテレータを生成するのだ。なので、これを通常通り他のイテレータと混ぜたり、forループで使用したりできる。
// 1から10までの整数を出力
for i in 1..11 {
println!("{}", i);
}
ベクターコンテナ
ベクターオブジェクトは、ヒープ領域に確保される有所有権参照である。従って、(Box<_>のように)ムーブ機構に順応している。固定長配列を値、配列スライスを無所有権参照とみなすと、RustのベクターコンテナはBox<_>ポインタに相当すると考えられる。
こうすれば、Vec<_>型をBox<_>型同様に、値というよりも一種のスマポのように考えやすくなる。配列スライスと同じく、長さはポインタに格納され、この場合、そのポインタとはベクターオブジェクトになる。
i32型のベクターはVec<i32>型になる。ベクターにリテラル表記はないが、vec!マクロを使えば同じ効果が得られる。また、Vec::new()メソッドで空のベクターも生成できる。
let v = vet![1, 2, 3, 4]; // 長さ4のVec上の例の2行目では、コンパイラがベクターの中身の型を把握できるように型注記が必須である。このベクターを使うつもりがあるなら、(翻訳者注: 使用箇所から型が推論できるので)この型注記は必要なくなるだろう。オブジェクト let v: Vec<i32> = Vec::new(); // i32型の空ベクター
配列や配列スライス同様、インデックス記法でベクターから値を取り出す(例: v[2])ことができ、これも境界値チェックが行われる。また、スライス記法でベクターをスライス化する(例: &v[1..3])こともできる。
ベクターにしかない機能として、サイズが動的に変化することが挙げられ、必要に応じて、伸びもするし、縮みもする。例えば、v.push(5)と書けば、ベクターの末尾に要素5を追加する(この時、変数vは可変でなければならない)。
なお、ベクターを肥大化させるとメモリの再確保が必要になることがあり、巨大なベクター相手だと、大量のコピーが発生することになる。これを防ぐには、with_capacityメソッドで予め大きな領域を割り当てておけばいい。詳しくはVec docs(英語版)を参照されたし。
Index trait
注意喚起: この節は、まだまともに解説できてない要素がたくさんある。チュートリアルを辿っているのなら、飛ばしても構わない。どう考えても、話題が高度すぎる。
配列やベクターと全く同じインデックス記法をHashMapなどのその他のコレクションクラスにも使用することができる。その上、自作のコレクションクラスにも応用できる。
インデックス記法(とスライス記法)の使用を宣言するには、Index traitを実装する。これは、Rustにおいて、組み込み型のみならず、ユーザ定義タイプでも略記法を使用できるようにする良い例になる(Derefでスマポの被参照を行え、その他Add traitなども含め、似たような挙動をする)。
Index traitは以下のような定義をしている。
pub trait Index<idx: ?Sized> {
type Output: ?Sized;
fn index(&self, index: Idx) -> &Self::Output;
}
Idxは添え字の型を表し、インデックス記法なら、だいたいusizeになり、スライス記法ならstd::ops::Range系のいずれかになる。Outputは、インデックス記法で返される型を表し、コレクションごとに異なる。スライス記法の時は、単一要素の型というよりも、スライスオブジェクトになる。
indexメソッドは、実際にコレクションから要素を取り出す役割を担っている。ちなみに、コレクションは参照で渡され、このメソッドは(翻訳者注: 要素と)同じライフタイムを持つ要素への参照を返す。
Vecクラスの実装を覗いて、どんな実装になるかを見てみよう。
impl<t> Index<usize> for Vec<t> {
type Output = T;
fn index(&self, index: usize) -> &T {
&(**self)[index]
}
}
前述した通り、インデックス記法はusize型を使っている。Vec<T>型オブジェクトに対して、インデックス記法だとT型の単一要素、つまりOutput型の値が得られる。indexメソッドの実装は少し奇妙だ。(**self)でベクター全体をスライス化し、インデックス記法で要素を取り出し、最後にこの要素への参照を取っている。
自作のコレクションクラスに対しても、同様のIndex実装を行って、インデックス記法やスライス記法を使うことができる。
初期化子記法
他の種々のデータ同様、Rustにおいて、配列とベクターは適切に初期化されなければならない。しばしば、0埋めされた配列が必要になるが、これをリテラル表記で書くのは苦痛でしかない。そのため、Rustには配列を特定値で初期化する糖衣構文が用意されており、[value; len]と書く。なので、長さ100の0埋め配列を作成するには、[0; 100]と書けば良い。
ベクターでも、vec![42; 100]と書けば、長さ100の42という値で埋められたベクターオブジェクトが得られる。
また、初期値は整数に限定されず、どんな式でもいい。
配列初期化式なら、長さは整数定数式でなければならず、vec!マクロなら、usize型の式にする。
原文: https://github.com/nrc/r4cppp/blob/master/arrays.md
2016年3月9日水曜日
C++プログラマー向けRust 翻訳シリーズ9
Destructuring(造語:非構造化)パート2 matchと無所有権参照
分解を行う際、無所有権参照が関わってくると、驚くことがあるだろう。無所有権参照について深く理解していれば驚くことはないと思うが、議論する価値はある(理解するには結構時間がかかる。間違いないよ。しかも、思ったよりも長いよ。だって、この記事の初稿はめちゃくちゃにしてしまったからね)。
&Enum型の変数xがあるところを想像してみよう(ここで、Enumは何らかのenum型だ)。
選択肢は二つある。*xにマッチをかけて全状態(Variant1 => ...など)を列挙するか、xにマッチさせて全状態への参照(&Variant1 => ...など)を列挙するかのどちらかだ。(流儀次第だが、一番目のやり方のほうが、見た目がすっきりして好ましいだろう)。
変数xは無所有権参照となり、これを被参照する方法には厳密なルールがある。そして、match式と絡むと(少なくとも筆者にとっては)驚異に変わるのだ。特に、既存のenumを一見問題なさそうな方法で変更していると、コンパイラがmatch式のどこかで爆発するのだ。
match式の真髄に触れる前に、Rustの値渡しに関するルールをおさらいしておこう。
C++では、値を変数に代入したり、関数に値を渡す方法は二通りある。値渡しと参照渡しである。
前者が、規定の方法であり、値はコピーコンストラクタ経由かビット単位でコピーされる。
引数や代入先を&記号で修飾すると、値は参照渡しされる。値のポインタだけがコピーされ、これに処理を施すと、コピー前の値にも影響を及ぼす。
Rustでも参照渡しができるが、渡し先と渡し元両方を&記号で修飾しなければならない。
Rustで値渡しをすると、さらにもう二つ選択肢ができる。コピー機構かムーブ機構かの選択だ。コピー機構ならば、C++の値渡しと同じになる(尤も、Rustにコピーコンストラクタはないが)。ムーブを行うと、値をコピーした上で、古い値を破棄する。Rustの型システムにより、古い値にはそれ以上アクセスできないことが保証される。具体的に、int型はコピー機構、Box<int>はムーブ機構である。
fn foo() {
let x = 7i;
let y = x; // xはコピーされる
println!("x: {}", x); // OK
let x = box 7i;
let y = x; // xはムーブされる
//println!("x: {}", x); // エラー: ムーブ済み(x)の値を使用しようとしている
}
Rustでは、オブジェクトがコピー機構かムーブ機構かは、デストラクタの有無で決定される。デストラクタはおそらく1投稿必要な話題だが、ひとまずDrop traitを実装していればデストラクタを持っていると言えるとだけ説明しておこう。C++と全く同じように、デストラクタはオブジェクトが破棄される直前に実行される。そして、デストラクタがあるオブジェクトはムーブ機構になる。もしデストラクタがないなら、全フィールドのどれか一つでもデストラクタが定義されていれば、オブジェクト全体がムーブ機構に適応する。そうして、オブジェクトツリーをたどっていき、どこにもデストラクタが見当たらなければ、そのオブジェクトはコピー機構に適応していると判断される。
さて、無所有権参照オブジェクトがムーブされていないことは重要である。その前提がないと、もはや妥当ではない古いオブジェクトへの参照を保有することになり、これは、スコープ外に抜けて破棄されたオブジェクトへの参照を保持しているのと等しい。一種のdanglingポインタである。
ポインタを保持していると、他にも同じオブジェクトを指す参照があるかもしれない。ゆえにオブジェクトがムーブ機構に適応している場合、ポインタを被参照するのは非安全である(一方、コピー機構に適応しているなら、被参照してもコピーを作るだけで古いオブジェクトはそのまま残り、他の参照も生き残り続ける)。
よし、じゃあmatch式に戻ろう。前述したように、&T型の変数xにマッチさせるには、match式内で1回だけ被参照するか、全項で参照にマッチさせるかの2通りの方法がある。
enum Enum1 {
Var1,
Var2,
Var3
}
fn foo(x: &Enum1) {
match *x { //選択肢1: ここで被参照
Var1 => {}
Var2 => {}
Var3 => {}
}
match x {
//選択肢2: 各項で被参照
&Var1 => {}
&Var2 => {}
&Var3 => {}
}
}
今回の場合、Enum1はコピー機構に適応しているので、どちらの手段も選択できる。順につぶさに見ていこう。一つ目の選択肢では、変数xをEnum1型の一時変数(変数xの値をコピーしている)に被参照し、Enum1型の各状態に対してマッチを行う。この方法は、値の型までは見ないから1段階マッチングになる。
一方、二番目の方法では、被参照は行わない。&Enum1型の値を各状態の参照とマッチさせることになる。こちらのマッチングは2段階である。まず、型(必ず参照)に対してマッチさせ、さらに参照化された型(Enum1)にマッチさせるのだ。
どちらにせよ、プログラマー(コンパイラ)がムーブや参照に関する不変性を保っていると確認せねばならない。参照されているオブジェクトは、一部であってもムーブしてはならないのだ。
もし対象の値がコピー機構に適応しているなら、これは大したことない。
しかし、ムーブ機構に適応している場合、全項でムーブが発生していないことを確認せねばならない。これを実現するには、ムーブが発生するデータを無視するか、参照を作ればいい(こうすれば、ムーブ渡しではなく参照渡しになる)。
enum Enum2 {
// Boxはデストラクタ持ちなので、Enum2はムーブ機構に適応している
Var1(Box),
Var2,
Var3
}
fn foo(x: &Enum2) {
match *x {
// 内包されたデータを無視するので、大丈夫
Var1(..) => {}
// 他項には変更なし
Var2 => {}
Var3 => {}
}
match x {
// 内包されたデータを無視するので、大丈夫
&Var1(..) => {}
// 他項には変更なし
&Var2 => {}
&Var3 => {}
}
}
いずれのアプローチでも、内包データを参照していないので、ムーブされることはない。一番目の手段で、変数xは参照されているものの、被参照のスコープ(つまり、match式全体)内で中身には触れていないから、何も逃すことはない。また、値自体を束縛(*xを変数に束縛するということ)してもいないので、このオブジェクトをムーブしてもいない。
二番目のアプローチで各項において参照を取ることができるが、被参照を行う一番目では無理だ。ゆえに、二例目の2番目の項をa @ &Var2 => {}と書き換える(ここで変数aは参照)ことはできるが、一例目でa @ Var2 => {}と書いてしまうと、*xを変数aにムーブすることになってしまうため不可能だ。
ref a @ Var2 => {}となら書き換えられる(ここで変数aもまた参照)が、あまり見かける記法ではない。
では、Var1に内包されたデータが必要ならばどうすればいいだろうか。以下のような書き方はできない。
match *x {
Var1(y) => {}
_ => {}
}
またはmatch x {
&Var1(y) => {}
_ => {}
}
理由は、どちらの場合も、変数xの一部を変数yにムーブすることになってしまうからだ。refキーワードを使用して、Var1内のデータへの参照を得ればいい。つまり、&Var1(ref y) => {}と書く。これならば、どこにも被参照はなく、変数xの一部をムーブすることにはならないからだ。その代わり、変数xの中身を指すポインタを作成することになる。
別の解決策として、Boxオブジェクトに分解することもできる(こうすると3段階マッチングになる)。つまり、&Var1(box y) => {}と書く。こうすると、int型はコピー機構に順応しており、変数yは、Var1内のBoxオブジェクトに包まれたint値のコピーになるからだ(この時、Var1はさらに無所有権参照になっている)。int型はコピー機構のため、変数xの一部をムーブする必要はない。
また、int値をコピーするのではなく、参照とすることもできる。つまり、&Var1(box ref y) => {}と書くのだ。この場合、どこにも被参照はなくなり、変数xの一部をムーブする必要がなくなるのだ。
仮にBoxオブジェクトの中身がムーブ機構に適応している場合、&Var1(box y) => {} と書くことはできず、参照バージョンを使わざるをえない。
さらにさらに、以前挙げた例の一番目においても、同様のテクニックを駆使することができ、いずれも先頭の&記号がなくなるだけの違いにしかならない。例えば、Var1(box ref y) => {}などね。
さあ、さらに複雑度を上げていこう。
1組のenum値に対してマッチングさせたいとする。こうなると、もう一番目のアプローチを取ることはできない。
fn bar(x: &Enum2, y: &Enum2) {
// エラー: 変数xとyはムーブされてしまう
// match (*x, *y) {
// (Var2, _) => {}
// _ => {}
// }
// OK
match (x, y) {
(&Var2, _) => {}
_ => {}
}
}
最初のアプローチは不正になる。理由は、マッチ対象の値が変数xとyを被参照することで作成され、新しいタプルオブジェクトにムーブされているからだ。そのため、今回は二番目のアプローチしかうまく動作しない。その上、言うまでもないことだが、上述したルールに従って変数xとyの一部をムーブしないようにしなければならない。もし、データに対して参照しか得られないにもかかわらず、値が必要な場合は、このデータをコピーする以外に手段はない。大体の場合、それはcloneメソッドを使うことを意味する。データがcloneメソッドを実装していない場合、さらに分解を行って手動でコピーを行うか、自分でcloneメソッドを実装することになる。
では、ムーブ機構を持った値への参照ではなく、値自体が存在する場合はどうだろうか。
今度は、ムーブしても大丈夫になる。なぜなら、他にこの値への参照がないことが明白になるからだ(もし、参照が存在していたら、コンパイラによって値の使用を制限されてしまう)。
fn bad(x: Enum2) {
match x {
Var1(y) => {}
_ => {}
}
}
ここでも、注意事項がある。まず、ムーブは1カ所でしか行えない。上記の例で、変数xとyの一部をムーブし、他は忘却の彼方へ追いやっている。しかし、仮にa @ Var1(y) => {}と書いて、変数xの全体を変数aに、変数xの一部を変数yにムーブしようとすると失敗する。このような項は不正なのだ。
しかも、変数aかyを参照に変えても無駄だ。こうすると今度は、以前解説した参照中ムーブ問題に直面してしまう。
変数aとy両方を参照に変えるのはありだ。これなら、いずれもムーブされず、変数xはそのまま残り、その全体と一部へのポインタを得られる。
同様に(かつ、より一般的に)、複数のデータを内包する状態がある場合、あるデータには参照をとって、他はムーブするという芸当は行えない。
具体的に言うと、Var4(Box<int>, Box<int>)と定義されたVar4があるとして、match項内でVar4(ref y, ref z) => {} という風に両方を参照したり、Var4(y, z) => {}という風に両者をムーブしたりすることはできるが、Var4(ref y, z) => {}という風に一方はムーブして、もう片方は参照するといったことはできないということである。
なぜなら、オブジェクトは一部でもムーブしたら全体が破棄されてしまい、参照が無効になってしまうからだ。
原文: https://github.com/nrc/r4cppp/blob/master/destructuring%202.md
2016年3月8日火曜日
C++プログラマー向けRust 翻訳シリーズ8
分解(造語:非構造化)
前回は、Rustのデータ型について見た。
一旦、何かしらデータ構造を手に入れたら、そこからデータを取り出したくなるだろう。
構造体について、RustではC++と全く同じようにフィールドアクセスができる。しかし、タプルやタプル構造体、enumについては、分解を行わねばならない(ライブラリには様々な便利関数があるが、それらも内部的には非構造化機能を使用している)。
データ構造の分解は、C++にはない機能だ。しかし、Pythonや種々の関数型言語で馴染み深いかもしれない。その根底にあるのは、ローカル変数でフィールド値を初期化し、データ構造を作成できるように、ローカル変数をデータ構造の値で初期化できるはずだという理論である。この単純な理論に端を発して、非構造化はRustで最も強力な機能の一つになった。
別の言い方をすると、非構造化は、パターンマッチングとローカル変数への代入を組み合わせたものである。
非構造化は、主にlet文かmatch文で行われる。対象のデータ構造が複数の状態を持つ(enumなど)場合は、match文を使用する。let式は現在のスコープに変数を展開するが、match式はそれ自身スコープを持っている。比較してみよう。
fn foo(pair: (int, int)) {
let (x, y) = pair;
// これでfoo内のどこからでもxとyを使用できる
match pair {
(x, y) => {
// こちらのxとyは、このスコープ内でしか使用できない
}
}
}
パターン(上記の例では、letキーワードの後と、=>記号の前に見られる)の書き方は、どちらのケースでも全く同じだ。このパターンは、関数定義の引数の位置でも使用することができる。fn foo((x, y): (int, int)) {
}
(この書き方は、タプルよりも構造体やタプル構造体に対して効果を発揮する)非構造化パターンは、ほとんどの初期化式内に書くことができ、無限に複雑化できる。この中には、データ構造のみならず、参照やリテラルも含まれる。
struct St {
f1: int,
f2: f32
}
enum En {
Var1,
Var2,
Var3(int),
Var4(int, St, int)
}
fn foo(x: &En) {
match x {
&Var1 => println!("一つ目"),
&Var3(5) => println!("三つ目 数字の5"),
&Var3(x) => println!("三つ目 数字の{}", x),
&Var4(3, St { f1: 3, f2: x }, 45) => {
println!("構造体入り。f2の中身は{}", x)
}
&Var4(_, x, _) => {
println!("別のVar4 f1の中身は{} f2の中身は{}", x.f1, x.f2)
}
_ => println!("その他(Var2)")
}
}
パターン内に&記号を使用して参照で分解する方法と、リテラル(5, 3, St{ ... })やワイルドカード(_)、変数(x)を組み合わせる方法に注目してほしい。パターン内でアイテムを一つ無視したい場合は、変数の代わりに_を記述すればいい。なので、中身の整数がどうでもよければ、&Var3(_)と書いてもいい。
最初のVar4項で中身の構造体を分解(ネストしたパターン)している。また、二番目のVar4項では、構造体全体を変数に束縛している。
その他、..と書くとタプルや構造体の全フィールドを表すことができる。したがって、enumの状態に応じて処理を行う必要はあるが、中身はどうでもいい場合は以下のようにも書くことができる。
fn foo(x: En) {
match x {
Var1 => println!("一つ目"),
Var2 => println!("二つ目"),
Var3(..) => println!("三つ目"),
Var4(..) => println!("四つ目")
}
}
構造体を分解する際、フィールドは定義時の順番通りに記述する必要はなく、..で残りのフィールドを省略することもできる。struct Big {
field1: int,
field2: int,
field3: int,
field4: int,
field5: int,
field6: int,
field7: int,
field8: int,
field9: int,
}
fn foo(b: Big) {
let Big { field6: x, field3: y, ..} = b;
println!("{}と{}を取り出したよ", x, y);
}
構造体の省略記法として、フィールド名を記述すれば、同名のローカル変数を定義してくれる。上記の例では、xとyという新しい変数を定義していたが、次のようにも書ける。fn foo(b: Big) {
let Big { field6, field3, ..} = b;
println!("{}と{}を取り出したよ", field3, field6);
}
今度は、フィールドと同じ名前のローカル変数を定義している。今回の場合は、field3とfield6ね。これ以外にもRustの分解機能にはテクニックが必要なものがある。
例えば、パターン内で変数を参照する必要が出たとしよう。この場合、&演算子は使用できない。これだと、参照を作成するんじゃなくて、参照にマッチしちゃうからね(ゆえに、オブジェクトを被参照することになる)。コードで表すとこう。
struct Foo {
field: &'static int
}
fn foo(x: Foo) {
let Foo { field: &y } = x;
}
ここで、変数yのタイプはintとなり、変数xのフィールドをコピーしている。パターン内で参照を作成するには、refキーワードを使う。
fn foo(b: Big) {
let Big { field3: ref x, ref field6, ..} = b;
println!("{}と{}を取り出したよ", *x, *field6);
}
ここで、変数xとfield6の型は&intになり、変数bのフィールドを参照している。最後のテクニックは、複雑なオブジェクトを分解する際に、個々のフィールドのみならず、中間のオブジェクトにも名前をつける必要が出た際に使うものだ。
前述の例で言うと、&Var4(3, St{f1: 3, f2: x }, 45)というパターンがあった。このパターン内では、あるフィールドだけに名前付けをしていたが、構造体オブジェクト全体に名前付けをする必要が出てくる可能性もある。
もちろん、&Var4(3, s, 45)と書けば、変数sに構造体オブジェクトを束縛できるが、こうしたら、フィールドアクセスにはもう1段階処理が必要になってしまうし、またフィールドが特定の値だった場合だけにマッチさせたいときにはmatch式をネストする必要が出てくる。これでは面白くない。
そこで、Rustでは、パターンの一部を@記号で名前付けすることができるようになっている。例として、&Var4(3, s @ St{f1: 3, f2: x }, 45)と書けば、フィールド(f2フィールドに対して変数x)と構造体オブジェクト全体(変数s)を変数に束縛できるのだ。
これにて、Rustのパターンマッチングの機能はほぼ網羅し終えた。まだベクターコンテナマッチングなどカバーしていない機能もあるけど、match式とlet文の使い方や強力な機能の一部でも理解してもらえてればありがたい。
次回は、match式と無所有権参照の些事たる関連性について解説する。これがまた、Rustを学習する上で間違いやすいんだよな〜。
原文: https://github.com/nrc/r4cppp/blob/master/destructuring.md
2016年3月7日月曜日
C++プログラマー向けRust 翻訳シリーズ7
データ型
この投稿では、Rustのデータ型について論じていこう。データ型は、ほぼC++のクラス、構造体、enumに等しい。Rustの方の違いは、データと振る舞いがC++(やJava、さらには他のオブジェクト指向言語)のものよりはるかに厳密に分けられていることにある。
振る舞いは関数によって定義され、関数はtraitとimpl(implementationのこと)両方で定義できるが、traitはデータを含むことができない。その点においてtraitは、Javaのインターフェースに近い。
traitとimplに関しては、また別記事を立てて解説する。今回は、データについてのみ述べる。
構造体
Rustの構造体はメソッドのないCやC++の構造体に似て、単純な名前付きフィールドのリストである。この見た目は、例をあげればよくわかるだろう。
struct S {
field1: int,
field2: SomeOtherStruct
}
ここで、2つフィールドのあるSという構造体を定義している。フィールドはカンマ区切りで記述する。好みによっては、最後のフィールドもカンマで区切らせることができる。構造体は型を導入する。上記の例では、識別子Sを型として使用することができる。SomeOtherStructは別の構造体という想定(上述の例では、型になっている)であり、(C++同様に)値としてフィールド化している。つまり、メモリ上にある構造体オブジェクトを指すポインタではないということ。
構造体のフィールドは、.演算子とフィールド名でアクセスする。以下、構造体の使用例。
fn foo(s1: S, s2: &S) {
let f = s1.field1;
if f == s2.field1 {
println!("field1 matches!")
}
}
引数s1は値渡しの構造体オブジェクト、引数s2は参照渡しの構造体オブジェクトである。メソッド呼び出し同様、.演算子だけで値渡しのオブジェクトだろうが、参照渡しのオブジェクトだろうが、フィールドにアクセスできる。->演算子を使用する必要はない。構造体は、構造体初期化式で作成する。構造体初期化式は、構造体名とフィールド値の組み合わせである。
fn foo(sos: SomeOtherStruct) {
let x = S { field1: 45, field2: sos }; // xを構造体初期化式で作成
println!("x.field1 = {}", x.field1);
}
構造体は、循環参照できない。つまり、宣言やフィールドの型名に繰り返して構造体名を使うことはできないということだ。これは、構造体の値の取り扱い方のせいである。故に例えば、struct R { r: Option<R> }は不正となり、コンパイルエラーが発生する(Option型について詳しくは後述)。そのような構造が必要ならば、何かしらポインタを使用しなければならない。ポインタならば、循環参照が許されている。
struct R {
r: Option<Box<R>>
}
上記の構造体にOption型を含めていないと、これをインスタンス化する手段がなくなって、コンパイラがエラーを吐くことになる。フィールドを持たない構造体は、定義においても、初期化においても、かっこは使用しない。ただ、宣言ではセミコロンで区切る必要がある。まあ、構文解析上の問題だけどね。
struct Empty;
fn foo() {
let e = Empty;
}
タプル
タプルは、名前のない、混種の連続データである。型としては、かっこに型名を並べて定義する。特に名前がないため、タプルは構造で識別される。例として、(int, int)は一組みのint、(int, f32, S)は3要素タプルである。タプルオブジェクトは、宣言と同じような書き方をするが、型の代わりに代入する値を入れ込むことで(4, 5)のように生成される。
// foo関数は、構造体を引数にとってタプルを返す
fn foo(x: SomeOtherStruct) -> (i32, f32, S) {
(23, 45.82, S { field1: 54, field2: x })
}
タプルは、let式で分解することで使うことができる。fn bar(x: (int, int)) {
let (a, b) = x;
println!("x was ({}, {})", a, b);
}
分解(非構造化)については、次回詳しく話す。タプル構造体
タプル構造体は、名前付きタプルである。また、逆の言い方をすれば、名前なしフィールドを持つ構造体とも表現できる。宣言は、structキーワード、かっこ内に型を記述し、セミコロンで閉じて行い、この名前が型として定義される。タプル構造体のフィールドは、名前よりも(タプル同様に)分解でアクセスする必要がある。
タプル構造体は、あまり使われない。
struct IntPoint (int, int);
fn foo(x: IntPoint) {
let IntPoint(a, b) = x; // 分解するのにタプル構造体の名前が必要なことに注目
println!("x was {(}, {})", a, b);
}
Enums
enum(イーナム)は複数の値を取りうるという点において、C++のenumや複合体(union)のような型である。最も単純なenumは、全くC++のenumと変わらない。
enum E1 {
Var1,
Var2,
Var3
}
fn foo() {
let x: E1 = Var2;
match x {
Var2 => println!("var2"),
_ => {}
}
}
しかしながら、Rustのenumはこれよりもはるかに強力である。各状態はデータを含むことができる。タプルのように、こちらは型を並べて定義する。こうなると、enumはC++のenumよりも複合体に近くなる。Rustのenumは(C++のような)タグなしのenumというよりは、タグ付きのenumだ。なので、enumの各状態を実行時に取り違えることがない。
enum Expr {
Add(int, int),
Or(bool, bool),
Lit(int)
}
fn foo() {
let x = Or(true, false); // xの型はExpr
}
Rustにおいて、多くのオブジェクト指向的多様性は、enumを使うことで取り回しが良くなる。enumを使う際、大抵はmatch式を使用する。
match式はC++のswitch文に似ていることを覚えているだろうか。match式や他の非構造化手段については、次回詳しく話すことにしよう。
fn bar(e: Expr) {
match e {
Add(x, y) => println!("Addノード: {} + {}", x, y),
Or(..) => println!("Orノード"),
_ => println!("それ以外(今回は、Litノード)"),
}
}
match式の各項が、Expr型の状態に合致しており、全状態が網羅されていなければならない。最終項(_)が、残りの全状態をカバーしている。まあ、今回はLitしかないけどね。各状態のデータは、変数に紐付けることができる。Add項で、変数xとyにデータを紐付けているのがその例だ。データに興味がなければ、..と書いておけばいい。例内のOr項でしているようにね。
Option型
Rustにおいて、特別よく使われるenumがOption型である。Option型には二つの状態がある。SomeとNoneだ。
Noneはデータを含まず、Someは型Tのフィールドを持っている(Option型はジェネリックなenumだ。これがどういうものか後ほど詳述するが、基本的なコンセプトはC++から明らかであると嬉しい)。
Option型は、データがあるかもしれないし、ないかもしれないという状態を表すのに使われる。C++で、何かしら未定義だったり、未初期化だったり、falseだったりする値を表すのにnullポインタを使ったが、Rustでは、Option型を使って表すのがベストであろう。
使用前に必ず値の存在確認を行わなければならないので、Option型は、より安全だ。nullポインタを被参照するようなことはない。その上、Option型は、より一般的であり、ポインタだけでなく、値に対しても使用することができる。
use std::rc::Rc;
struct Node {
parent: Option<Rc<Node>>,
value: int
}
fn is_root(node: Node) -> bool {
match node.parent {
Some(_) => false,
None => true
}
}
ここで、parentフィールドはNoneかRc<Node>型の値を持つSomeたりうる。例では、この内包データを使用していないが、実際は違うだろう。Option型には、便利なメソッドがある。なので、is_root関数の本体は、node.is_none()や!node.is_some()とも書ける。
可変性の伝播とCell/RefCell
Rustのローカル変数は、標準で不変となり、mut属性を付けることで可変にできる。構造体やenumのフィールドに属性付けは行わない。こちらの可変性は伝播するのだ。要するに、構造体のフィールドが可変か不変になるかは、構造体のオブジェクト自体が可変か不変かに依存するということだ。
struct S1 {
field1: int,
field2: S2
}
struct S2 {
field: int
}
fn main() {
let s = S1 { field1: 45, field2: S2 { field: 23 } };
// sは内部も不変である。以下のような値の変更は行えない
// s.field1 = 46;
// s.field2.field = 24;
let mut s = S1 { field1: 45, field2: S2 { field: 23 } };
// sは可変なので、今度はOK
s.field1 = 46;
s.field2.field = 24;
}
Rustの可変性の伝播は、参照には及ばない。これは、C++において、constなオブジェクトからポインタ経由でconstでないオブジェクトを変更できるのと似ている。変更可能な参照をフィールドに含めるには、フィールドの型宣言に&mut属性を付ける必要がある。struct S1 {
f: int
}
struct S2<'a> {
f: &'a mut S1 // 変更可能な参照
}
struct S3<'a> {
f: &'a S1 // 変更不可能な参照
}
fn main() {
let mut s1 = S1{f:56};
let s2 = S2 { f: &mut s1};
s2.f.f = 45; // s2は不変だけど、問題ない
// s2.f = &mut s1; // 問題あり - s2は可変ではない
let s1 = S1{f:56};
let mut s3 = S3 { f: &s1};
s3.f = &s1; // 問題ない - s3は可変だから
// s3.f.f = 45; // 問題あり - s3.fは不変
}
(S2とS3に付いている'aというパラメータは、ライフタイムを表す。もうすぐ解説します)たまに、オブジェクト自体は論理的に不変(翻訳者注:原文ではmutableだがimmutableのtypoか?)なのに、内部的に可変であるべき部分を含むことがある。キャッシュや参照カウントを考えてほしい(これらは、本当の不変性はもたらさない。なぜなら、参照カウントの変更の効果が、デストラクタ経由で出てくるからだ)。
C++でなら、mutableキーワードを使ってオブジェクト自体がconstであっても、変更することができる。Rustでは、CellかRefCell構造体が使える。これらを使って不変なオブジェクトの一部を可変にすることができる。これは便利だが、ともすると、Rustで不変なオブジェクトを見かけたら注意を要するということである。一部が実際は可変かもしれないからね。
CellやRefCellを使えば、Rustの可変性や代入性に関する厳密なルールを回避することができる。CellやRefCellを使用するのは安全だ。なぜなら、コンパイラが静的にRustの不変性をチェックできなくても、実行時に保ってくれるからだ。CellもRefCellもシングルスレッド向けオブジェクトである。
Cell型は、コピー機構を持つ型(組み込み型のことだ)に使おう。Cell型には、保持している値を変更するget,setメソッドと、値でインスタンスを初期化するnewメソッドがある。
Cell型は、とても単純なオブジェクトだ。つまり、コピー機構を持つオブジェクトは(Rustでは)他に参照されるはずがなく、スレッド間で共有されることがないため、何も高度なことをする可能性がない。おかしな事態になりようがないのだ。
RefCell型は、ムーブ機構を持つ型に使おう。これは、Rustのほぼ全てを意味し、構造体オブジェクトがよく使われる例だ。
RefCell型もnewメソッドを使って生成し、setメソッドを持っている。RefCellオブジェクトの値を取り出すには、メソッド(borrow, borrow_mut, try_borrow, try_borrow_mut)を使って無所有権参照しなければならない。これらのメソッドは、RefCellに格納されたオブジェクトへの所有権なし参照を返す。
また、これらのメソッドも静的参照と同じルールに従い、可変無所有権参照は一つしか作れず、可変なものと不変なものを同時には作成できない。ただ、コンパイルエラーではなく、実行時エラーになる。
try_で始まるメソッドは、Option型を返し、成功時にはSome(val)を、失敗時にはNoneを得る。
値が参照されている間は、setメソッド呼び出しは失敗する。
次の例では、参照カウント式ポインタでRefCellオブジェクトを参照している(よく使われるユースケース)。
use std::rc::Rc;
use std::cell::RefCell;
struct S {
field: int
}
fn foo(x: Rc<RefCell<S>>) {
{
let s = x.borrow();
println!("フィールド、2回参照 {} {}", s.f, x.borrow().field);
// let s= x.borrow_mut(); // エラー - xの中身はすでに無所有権参照中
}
let s = x.borrow_mut(); // OK。先ほどの無所有権参照はすでにスコープ外
s.f = 45;
// println!("フィールド {}", x.borrow().field); // エラー - 同時に可変参照と不変参照はできない
println!("フィールド {}", s.f);
}
CellかRefCellオブジェクトを使っているなら、なるべく小さなオブジェクトに配置すべきだ。つまり、構造体全体に置くよりも構造体の数フィールドに配置するのを選べということだ(翻訳者注:原文を読んでもよく意味が読み取れなかった)。これらはシングルスレッドのロックオブジェクトと見なせばいい。1回のロックで処理がかち合う可能性が減るから、洗練されたロックの方がいい。
原文: https://github.com/nrc/r4cppp/blob/master/data%20types.md
2016年3月4日金曜日
C++プログラマー向けRust 翻訳シリーズ6
参照カウント式ポインタ、生ポインタ
ここまでで、所有権ありポインタと所有権なしポインタをカバーしてきた。所有権ありポインタは新規格C++のstd::unique_ptrに酷似している。また、所有権なしポインタは、C++でポインタや参照を使用していた人が行き着くデフォルトのポインタである。
Rustには他にも、より使う頻度の低いポインタが標準ライブラリや言語組み込みで存在する。これらは、C++で聞き馴染みのあるスマポによく似ているものだ。
この投稿を書き上げるのに、だいぶ時間がかかってしまったが、未だに気に入っていない。投稿自体とRustの機能自体に締まりがない部分が多々あるからだ。記事はブラッシュアップし、Rustの機能も言語の進化とともに改良されるといいね。
現在Rustを学習中の人は、この部分を飛ばしたいと思うかもしれないし、必要ない方がいい。他のポインタについて解説したから、ここに記してあるだけなのだ。
Rustにはいろんな種類のポインタがあるような気がするかもしれないが、C++でも標準ライブラリ内に多種多様なスマポがあることを鑑みれば、よく似ている。
ただRustでは、言語を学び始めてからそれらのポインタに遭遇する機会が多い。Rustのポインタはコンパイラのサポートがあるので、誤使用が少なくなるからね。
所有権ありポインタや所有権なしポインタほど、これらのポインタについて深入りするつもりはない。率直に言って、それほど重要ではないからね。
もしかしたら、後で補足するかもしれないけど。
参照カウント式ポインタ
参照カウント式ポインタは、標準ライブラリのstd::rcモジュール(モジュールについてはそろそろ触れる。例にuseという呪文が含まれているのはこのモジュールのためだ)に含まれている。T型のオブジェクトに対する参照カウント式ポインタの型は、Rc<T>となる。
参照カウント式ポインタは、スタティックメソッド(今のところはC++のものと同じと考えておいていいが、後々少しだけ違うとわかる)を使用して作成する。Rc::new(...)という参照先のオブジェクトを引数に取るメソッドだ。
このコンストラクタメソッドは、Rustの通常のmove/copy機構に倣っている(所有権ありポインタについて議論したのと同様に)。どちらにしても、Rc::newメソッドを呼ぶと、その値にはポインタ経由でしかアクセスできなくなるのだ。
他種のポインタと同じく、.演算子で必要な被参照は全て行うことができる。*演算子を使えば、手動で被参照することもできる。
参照カウント式ポインタを実引数にするには、cloneメソッドを呼ぶ必要がある。これはめんどくさいし、できれば直したいところだが、確定ではない(残念ながらね)。参照先の値に対して(無所有権)参照を取ることもできるから、それほどcloneメソッドを呼ぶ必要はないはずだ。
Rustの型システムにより、全参照がなくなるまで参照カウントされている変数が削除されないことが保証されている。また、参照を取ることには一々、参照カウントを増減させる必要がなく、パフォーマンスが上がるという別の利点もある(ただ、この違いはさほど重要ではないだろう。なぜなら、参照カウント式ポインタは、シングルスレッドに限定されているため、参照カウントが原始的な処理である必要がないからだ)。C++同様、参照カウントポインタ自体への参照を作成することもできる。
参照カウント式ポインタの例をあげよう。
use std::rc::Rc;
fn bar(x: Rc<i32>) { }
fn bad(x: &i32) { }
fn foo() {
let x = Rc::new(45);
bar(x.clone()); // 参照カウントをインクリメント
bad(&*x); // こちらはインクリメントしない
println!("{}", 100 - *x);
} // このスコープが終了すると、全参照カウントオブジェクトがなくなり、
// 参照カウントが0になるので、メモリー領域が解放される
参照カウント式ポインタは、必ず不変になる。可変な参照カウント式のオブジェクトが必要なら、RefCell(かCell)オブジェクトをRcポインタに包む必要がある。CellとRefCellオブジェクト
CellとRefCellオブジェクトは、可変性ルールを誤魔化せる構造体だ。
まず、Rustの構造体と構造体の可変性ルールをカバーしないと説明するのはなんとなく難しいので、このちょっとトリッキーなオブジェクトの説明は後回しにしよう。
とりあえず、可変な参照カウント式オブジェクトが必要なら、CellかRefCellオブジェクトをRcポインタに包まなければならないということを知っておけばよい。勘として、組み込み型のデータにはCellオブジェクトを、move機構のあるオブジェクトにはRefCellオブジェクトを使えばいいだろう。
したがって、可変かつ参照カウント式のint型変数は、Rc<Cell<int>>と表せる。
*T - 生ポインタ
最後に、Rustには2種類の生ポインタ(またはアンセーフポインタとも呼ばれる)がある。不変な生ポインタを表す*const Tと、可変な生ポインタを表す*mut Tだ。生ポインタも&か&mut演算子を使用して作成する(&演算子は、無所有権参照と生ポインタのどちらにも使われるので、&Tではなく*Tを得るには型を明示しなければいけない可能性がある)。
生ポインタは、Cのポインタに似ている。特に使用制限のないメモリ領域を表すだけのポインタだ(ポインタ演算はキャストをしなければできないが、どうしようもない時はキャストすればポインタ演算が行える)。
生ポインタは、Rustにおいて唯一nullになりうるポインタだ。生ポインタには自動被参照と自動参照機能は存在しない(なので、メソッド呼び出しは(*x).foo()と書かなければならない)。最重要な制限は、生ポインタがunsafeブロック外では被参照できず、使えないことにある。ノーマルなRustのコード内では、持ち回ることしかできない。
では、アンセーフコードとはなんだろうか?Rustには強力な型制限がかかっている。そのために稀にではあるが、すべきことができない時が出てくる。
Rustの目的は、システム記述用言語なので、可能なことはなんでもできなければならない。それは時として、コンパイラが安全を保証できないような事柄も行うことを意味する。
これを実現するために、Rustにはunsafeブロックという概念が存在し、unsafeキーワードによって使用する。
アンセーフコード内では、非安全な事柄が行える。生ポインタを被参照したり、配列の境界値チェックを行わずにアクセスしたり、Foreign Function Interface経由で他言語で書かれたコードを呼び出したり、変数をキャストすることだ。
アンセーフコードを書くにはノーマルなRustのコードを書くよりもずっと注意を要することは一目瞭然だろう。事実、アンセーフコードを書かなければいけない機会なんて皆無に等しい。ほとんどユーザコードよりもライブラリ内でちょこっと使うだけに終わる。
アンセーフコードでは、C++で安全性を確保するために普通にしていたことと同じことをしなければならない。さらに、普段ならコンパイラが保証してくれる不変性を手動で保たなければならない。アンセーフブロックはRustの不変性を保つ努力をさせてくれるのであって、不変性を破らせてくれるわけではない。そんなことをしたら、通常コードとアンセーフコード両方にバグを生み出してしまう。
さて、生ポインタの例だ。
fn foo() {
let mut x = 5;
let x_p: *mut i32 = &mut x;
println!("x+5={}", add_5(x_p));
}
fn add_5(p: *mut i32) -> i32 {
unsafe {
if !p.is_null() { // 生ポインタは自動被参照できないので、このメソッドは*i32のもの。
// i32のものではないことに注目してほしい
*p + 5
} else {
-1 // 褒められたエラー処理法ではない
}
}
}
これにて、Rustのポインタを巡る旅は終了だ。次回は、少しポインタから離れて、Rustのデータ構造を見ていこう。まあ、また無所有権参照のところで戻ってくるんだがね。原文: https://github.com/nrc/r4cppp/blob/master/rc%20raw.md
2016年3月3日木曜日
C++プログラマー向けRust 翻訳シリーズ5
所有権なしポインタ
前回の投稿では、所有権ありポインタを紹介した。今回は、Rustのプログラムではるかに一般的な他のポインタについて語ろう。所有権なしポインタ(所有権なしの参照または単に参照とも呼ばれる)だ。
既存のデータに対する参照を作りたい(所有権ありポインタのようにヒープ領域にデータを確保し、そのデータを参照するのではない)場合、&記号(所有権なし参照)を使う必要がある。
所有権ありポインタとなしポインタがRustにおいて、最も一般的なポインタと言えるだろう。そして、C++のポインタや参照に当たるものが必要なら(例えば、関数に引数を参照渡しするなど)、所有権なしポインタがこれに当たるかもしれない。
&演算子を使用して所有権なしポインタを作ったり、参照型を表し、*演算子でその値を取る。所有権ありポインタと同じ、自動被参照のルールが適用される。
fn foo() {
let x = &3; // タイプ: &i32
let y = *x; // 3, タイプはi32
bar(x, *x);
bar(&y, y);
}
fn bar(z: &i32, i: i32) {
// ...
}
&演算子はメモリー割り当てを行わない(既存のデータに対する所有権なし参照しか作成できない)。そのため、所有権なし参照がスコープ外に抜けても、メモリー解放は行われない。所有権なし参照は、固有ではない。換言すれば、同じ値を指す参照を複数作れるということだ。
fn foo() {
let x = 5; // タイプ: i32
let y = &x; // タイプ: &i32
let z = y; // タイプ: &i32
let w = y; // タイプ: &i32
println!("全部5のはず: {} {} {}", *w, *y, *z);
}
値同様、所有権なし参照も標準では不変である。&mut演算子を使用して可変な参照を取ったり、可変参照タイプを表したりできる。可変所有権なし参照は固有である(一つの値を指す可変参照は一つしか作れず、可変参照を作れるのは不変な参照がないときに限る)。不変参照が必要な箇所には可変参照を当てはめることができるが、その逆は不可能である。
さて、上記の性質を例でおさらいしてみよう。
fn bar(x: &i32) { ... }
fn var_mut(x: &mut i32) { ... } // &mut i32は可変なi32型の値に対する参照
fn foo() {
let x = 5;
//let xr = &mut x; // エラー: 不変変数に対して可変参照は作れない
let xr = &x; // OK(不変な参照を作成)
bar(xr);
//bar_mut(xr); // エラー: 可変参照が必要
let mut x = 5;
let xr = &x; // OK(不変な参照を作成)
//*xr = 4; // エラー: 不変参照を可変化しようとしている
//let xr = &mut x; // エラー: すでに不変参照が存在するので、可変参照は作れない
let mut x = 5;
let xr = &mut x; // OK(可変参照を作成)
*xr = 4; // OK
//let xr = &x; // エラー: すでに可変参照があるので、不変参照は作れない
//let xr = &mut x; // エラー: 同時に二つ以上可変参照は存在できない
bar(xr); // OK
bar_mut(xr); // OK
}
参照自体は、参照を保持する変数の可変性にかかわらず、可変にも不変にもなることに注目してほしい。これは、C++において、ポインタが、参照するデータの可変性にかかわらず、定性になったり不定性になったりすることと似ている。また、所有権ありポインタとは対称的である。なぜなら、所有権ありポインタの可変性は、データに紐付いているからだ。
fn foo() {
let mut x = 5;
let mut y = 6;
let xr = &mut x;
//xr = &mut y; // エラー: xrは不変
let mut x = 5;
let mut y = 6;
let mut xr = &mut x;
xr = &mut y; // OK
let mut x = 5;
let mut y = 6;
let mut xr = &x;
xr = &y; // OK: 参照されているデータは可変ではないのに、xrは可変
}
可変な値を所有権なし参照すると、参照が存在する間だけ不変になる。一旦、所有権なしポインタがスコープ外になると、先ほどの値は再び可変になる。この挙動も、所有権ありポインタと対称的だ。所有権ポインタは、所有権が移ってしまったら、前の参照は二度と使用できない。fn foo() {
let mut x = 5; // タイプ: i32
{
let y = &x; // タイプ: &i32
//x = 4; // エラー: xは参照中
println!("{}", x); // OK: xの読み出しは可能
}
x = 4; // OK: yはもう存在しない
}
値に対して、可変な参照を取ろうとした場合にも同じ事態に遭遇する。それでも、値は変更できないのだ。一般にRustでは、データは一つの変数または、ポインタ経由でしか変更できない。
その他、今度は可変参照を保持しているので、不変参照を取ることはできない。これは、対象の値の使用法の制限になる。
fn foo() {
let mut x = 5; // タイプ: i32
{
let y = &mut x; // タイプ: &mut i32
//x = 4; // エラー: xは参照中
//println!("{}", x); // エラー: xを参照する必要がある
}
x = 4; // OK: yはもう存在しない
}
C++と異なり、Rustでは値は自動的に参照されることはない。ゆえに、参照渡しの引数を持った関数がある場合、呼び出し側で実引数を参照しなければならない。とは言ったが、ポインタ型は、参照に自動変換される。
fn foo(x: &i32) { ... }
fn bar(x: i32, y: Box<i32>) {
foo(&x);
// foo(x); // エラー: i32ではなく、&i32が必要
foo(y); // OK
foo(&*y); // これもOK。より明確だが、いい書き方ではない
}
mut vs const
この段階で、Rustのmut属性とC++のconst属性は比較する価値があるだろう。
一言で言うと、両者は真逆である。
Rustにおいて、値は標準で不変であり、mut属性を使って可変にすることができる。
一方、C++において、値は標準で可変であり、const属性を使用して定数にできる。
より気付きにくいが重要な差異は、C++における定性は、1回限りしか有効でないのに対して、Rustの不変性は普遍であるということだ。したがって、C++でconstな変数を定義したとしても、誰か別の人がその変数へのconstでない参照を取って、定義者に知られずに変更することができる。しかし、Rustでは不変な変数を定義したら、二度と変わらないと保障されている。
前述したように、可変な変数はすべて固有である。そのため、可変な値があったら、自分で変えない限り、変わることはない。また、誰も、それが不変であることを頼りにしていないとわかってるので、自由に変更することができる。
所有権なし参照とライフタイム(生存期間)
Rustの安全性に関する大きな目的は、nullポインタ(ポインタが削除されたメモリー領域を指すこと)を避けることだ。Rustでは、無効な参照は存在しえない。参照自体よりも長生きするメモリー領域を所有権なし参照することだけが有効なのだ(まあ、少なくとも参照である限りは)。別の言い方をすると、参照の生存期間は、参照される値の生存期間よりも短くなければならないということだ。
上記の制限は、今までの例すべてで守られている。
{}記号で導入されるスコープや、関数は生存期間に基づいて紐付いている。つまり、変数がスコープ外に抜けると、そのライフタイムが終了したことと等しいということである。
自分よりも生存期間が短いものへの参照を生成しようとする(例として、スコープが短い場合など)と、コンパイラがエラーを吐く。
fn foo() {
let x = 5;
let mut xr = &x; // OK: 変数xと変数xrは生存期間が同じ
{
let y = 6;
//xr = &y; // エラー: 変数xrは変数yよりも生存期間が長い
} // 変数yはここで解放される
} // 変数x,xrはここで解放される
上記の例で変数x,xrは変数xrの方が下に記述されているので、生存期間が異なるが、より気をひくのは生存期間の終了のタイミングである。なぜなら、どんなに頑張っても存在しない変数は参照できないからだ。Rustにより強制され、C++よりも安全性を高められる事柄である。ライフタイムの明示
しばし、所有権なし参照を試してみた結果、ライフタイムが明示された所有権なしポインタに行き当たったことがあるだろう。
これは、&a' T(比較: &T)と書き、別に記事を立ち上げるほどのビッグトピックになる類の話だ。ちょうど、生存期間の多様性を説明しないといけないしね(まあ、その前にもっと一般的でないポインタの話があるけど)。
ひとまず、&Tは&'a Tの略記であり、aは型Tが宣言されているスコープを指すとだけ述べておきたい。
原文: https://github.com/nrc/r4cppp/blob/master/borrowed.md
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
C++プログラマー向けRust 翻訳シリーズ3
組み込み型、演算子
RustにもC++と全く同じ数値演算および論理演算子が存在する。bool型は両言語で同じだ(true、falseリテラルも同じだ)。
Rustにも整数型、非負整数型、浮動小数点数型の概念が存在する。しかし、見た目は少し異なる。
Rustでは、整数を表すのにintを使い、自然数を表すのにuintを使用し、どちらもポインタのサイズと等しい。例として32ビットシステムならば、uintは32ビットサイズの非負整数型になる。
さらにRustにはサイズを明示した整数型も存在し、こちらはuまたはiの後ろに8,16,32,64の数字をつけて表す。したがって、例えば、u8と書いたら、8ビットの非負整数型、i32と書いたら、32ビットの整数型を意味する。浮動小数点数なら、f32とf64がある。
数値リテラルは、接尾辞をつけて型を表現することができる(型のint,uintではなくu,iを用いる)。接尾辞がなければ、コンパイラが型の推論を試みる。推論にも失敗したら、intかf64(小数点がある場合)になる。
fn main() {
let x: bool = true;
let x = 34; // type int
let x = 34u; // type uint
let x: u8 = 34u8;
let x = 34i64;
let x = 34f32;
}
メモ書き:Rustでは、変数の再定義ができる。ゆえに上記のコードは正しい。つまり、個々のlet文が新変数xを定義し、直前のものを隠蔽するのだ。この性質は、変数が標準で不変的であるため、思った以上に役に立つ。
数値リテラルは、10進数以外にも2進数、8進数、16進数で記述することができる。それぞれ0b,0o,0xという接頭辞を使えばいいのだ。
数値リテラル内にアンダースコアを入れることもでき、このアンダースコアはコンパイル時に無視される。
fn main() {
let x = 12;
let x = 0x1100;
let x = 0o14;
let x = 0xe;
let y = 0x_1100_0011_1011_0001;
}
Rustにも、文字型と文字列型があるが、こちらはユニコードでエンコードされているため、C++のものとは少し異なる。これらについて語るのは、ポインタと参照、ベクターコンテナ(配列)を導入してからにしよう。Rustでは、数値型は勝手に丸められない。一般に、Rustでは、C++よりもはるかに無断で精度落ちやキャストが発生することが少ない。明示的に丸め込みやキャストを行うにはasキーワードを使う。
数値型は数値型にならキャストできる。asキーワードでは、論理値と数値型の相互変換はできない。
fn main() {
let x = 34u as int; // uintからintへキャスト
let x = 10 as f32; // intからfloatへ
let x = 10.45f64 as i8; // floatからintへ(精度落ち)
let x = 4u8 as u64; // 精度アップ
let x = 400u16 as u8; // 144と精度が落ちる(値自体変わってしまっている)
println!("`400u16 as u8` gives {}", x);
let x = -3i8 as u8; // 253,符号ありからなしへ(符号変え)
println!("`-3i8 as u8` gives {}", x);
//let x = 45u as bool; // 不可能!
}
Rustには以下に挙げる演算子がある。数値演算子: +, -, *, /, %
ビット演算子: |, &, ^, <<, >>
比較演算子: ==, !=, >, <, >=, <=
論理演算子: ||, &&
演算子の挙動自体はC++と同じだが、Rustのものは適用できる型が少し限定的になっている。ビット演算子は、整数型にしか適用できないし、論理演算子も論理値にしか適用できない。
Rustには単項の-演算子が存在し、数値の符号を反転させる。!演算子は、論理値を反転させ、整数型の全ビット値を反転させる(後者の意味ではC++の~演算子と等価)。
また、RustにもC++同様、各種代入演算子(+=など)が存在するが、インクリメント、デクリメント(++など)は存在しない。
原文: https://github.com/nrc/r4cppp/blob/master/primitives.md
2016年2月28日日曜日
C++プログラマー向けRust 翻訳シリーズ2
制御フロー
If
if文は、C++のものと全く一緒だ。一つ違いがあるとしたら、大かっこは必須であり、条件式を囲むかっこはそうではないことだ。
また、Rustのはif式であるため、C++の三項演算子と同じように使用することができる(前回、ブロックの最後の式がセミコロンで閉じていなければ、その値がブロックの戻り値になることを思い出してほしい)。
Rustには三項演算子は存在しない。ゆえに、下記の関数は双方とも同じ挙動をする。
fn foo(x: i32) -> &'static str {
let mut result: &'static str;
if x < 10 {
result = "less than 10";
} else {
result = "10 or more";
}
return result;
}
fn bar(x: i32) -> &'static str {
if x < 10 {
"less than 10"
} else {
"10 or more"
}
}
一番目のコードは、C++のコードの直訳形だ。二番目がRustでは好ましい。let x = if ...などとすることもできる。
ループ
RustにもまたC++のようにwhileループがある。
fn main() {
let mut x = 10;
while x > 0 {
println!("Current value: {}", x);
x -= 1;
}
}
Rustには、do...whileループはないが、単に永久ループするloop文がある。fn main() {
loop {
println!("Just looping");
}
}
RustにもC++同様、breakとcontinueが存在する。Forループ
Rustにもforループが存在する。しかし、こちらは少し勝手が違う。
整数型のvectorコンテナがあって、すべて表示したいとしよう(vectorコンテナ、配列、イテレータ、およびgenericsについては後ほど詳述する。とりあえず、Vec<T>はT型のシーケンスであり、iter関数は順繰りに処理できるものからイテレータを返すものと思っておけばいい)。
シンプルなforループは、以下のような見た目をしている。
fn print_all(all: Vec<i32>) {
for a in all.iter() {
println!("{}", a);
}
}
all変数のインデックスをたどりたい場合(よりC++の配列に作用するforループ風に)、以下のようにも書ける。
fn print_all(all: Vec<i32>) {
for i in ..all.len() {
println!("{}: {}", i, all.get(i));
}
}
len関数が何をするかは容易に想像が付くだろう。Switch/Match
Rustには、match式がある。これはC++のswitch文に似ているが、ずっと強力だ。
以下のシンプルバージョンは、馴染み深く感じられるだろう。
fn print_some(x: i32) {
match x {
0 => println!("x is zero"),
1 => println!("x is one"),
10 => println!("x is ten"),
y => println!("x is something else {}", y),
}
}
見た目に違いがある。評価された値と実行する式の対応付けに=>という記号を使っているし、match節の区切りに,を使用している(最後の,はあってもなくてもいい)。さらに意味にもあまり明瞭でない違いがある。評価値は網羅性がなければならない。つまり、評価対象の式(上記の例では変数x)がマッチする可能性のある値すべてが記述されていなければならない。
y => ...の行を削除してどうなるか見てみるといい。そうなるのは、0と1と10にマッチする値しか指定されておらず、未指定の値が数多くあるからだ。最後の節で変数yが評価値(今回のケースでは変数x)に紐づけられている。
次のようにも記述できる。
fn print_some(x: i32) {
match x {
x => println!("x is something else {}", x)
}
}
ここで、match節内の変数xは、引数のxを隠蔽している。これは、内側のスコープで変数を宣言するのと同じだ。変数に名前をつける必要がなければ、無名変数に_とつけることができる。これは、ワイルドカードマッチを行うのと同じだ。
何もする必要がなければ、空の節を用意すればいい。
fn print_some(x: i32) {
match x {
0 => println!("x is zero"),
1 => println!("x is one"),
10 => println!("x is ten"),
_ => {}
}
}
他にも次のmatch節へのフォールスルーがないという意味の違いもある。matchは非常に強力だと後ほどの投稿で認識できるだろう。今のところは、もう二つばかり、その機能を紹介しておきたい。or演算子と限定if節だ。
次の例で自明だと嬉しい。
fn print_some_more(x: i32) {
match x {
0 | 1 | 10 => println!("x is one of zero, one, or ten"),
y if y < 20 => println!("x is less than 20, but not zero, one, or ten"),
y if y == 200 => println!("x is 200 (but this is not very stylish)"),
_ => {}
}
}
if式同様、match文も実は式なので、先ほどの例は以下のように書き換えられる。fn print_some_more(x: i32) {
let msg = match x {
0 | 1 | 10 => "x is one of zero, one, or ten",
y if y < 20 => "x is less than 20, but not zero, one, or ten",
y if y == 200 => "x is 200 (but this is not very stylish)",
_ => "something else"
};
println!("x is {}", msg);
}
閉じかっこの後のセミコロンに注目してほしい。これは、let文が文であり、let msg = ...;という形にしなければいけないからだ。右辺にmatch式を代入しているが(match式は通常セミコロンで閉じる必要はない)、let文には必要なのだ。いつも引っかかるんだよな〜。
理由付け:Rustのmatch文では、C++のswitch文でありがちなバグ(breakキーワードを忘れたり、意図なしにフォールスルーすることがない)を回避することができる。また、enum(詳しくは後ほど)に値を追加したら、コンパイラがmatch文に含まれているか確認してくれる。
メソッド呼び出し
最後に、ちょこっとだけRustにもC++のようにメソッドが存在することに注目しよう。Rustのメソッドは必ず.演算子で呼び出す(->演算子はない。これについてはまた別立てで解説する)。すでに数個(len,iter)例が挙がっていた。
定義の仕方や呼び出し方についての詳述は、また将来することにしよう。C++やJava流の推測は概ね当たっているよ。
原文: https://github.com/nrc/r4cppp/blob/master/control%20flow.md
原文: https://github.com/nrc/r4cppp/blob/master/control%20flow.md
登録:
投稿 (Atom)