基本
語り手: このセクションには根本的な誤りが潜んでいます。というのも、それこそがこの本全体の要点だからです。ただし、いったん
unsafeを使い始めると、誤ったことをしていてもすべてがコンパイルされ、一見動作しているように見えることがあります。その根本的な間違いは次のセクションで特定します。このセクションの内容を実際のプロダクションコードで使わないでください!
さて、基本に戻りましょう。リストをどのように構築すればよいでしょうか?
以前は単にこうしていました。
impl<T> List<T> {
pub fn new() -> Self {
List { head: None, tail: None }
}
}
しかし、もう tail には Option を使っていません。
> cargo build
error[E0308]: mismatched types
--> src/fifth.rs:15:34
|
15 | List { head: None, tail: None }
| ^^^^ expected *-ptr, found
| enum `std::option::Option`
|
= note: expected type `*mut fifth::Node<T>`
found type `std::option::Option<_>`
Option を使うこともできるのですが、Box とは異なり、*mut は nullable です。つまり、null ポインタ最適化の恩恵を受けられません。代わりに、None を表すために null を使います。
では、どうやって null ポインタを得るのでしょうか?いくつか方法はありますが、私は std::ptr::null_mut() を使うのが好みです。望むなら 0 as *mut _ を使うこともできますが、それはちょっとごちゃごちゃしているように見えます。
use std::ptr;
// 定義...
impl<T> List<T> {
pub fn new() -> Self {
List { head: None, tail: ptr::null_mut() }
}
}
cargo build
warning: field is never used: `head`
--> src/fifth.rs:4:5
|
4 | head: Link<T>,
| ^^^^^^^^^^^^^
|
= note: #[warn(dead_code)] on by default
warning: field is never used: `tail`
--> src/fifth.rs:5:5
|
5 | tail: *mut Node<T>,
| ^^^^^^^^^^^^^^^^^^
warning: field is never used: `elem`
--> src/fifth.rs:11:5
|
11 | elem: T,
| ^^^^^^^
warning: field is never used: `head`
--> src/fifth.rs:12:5
|
12 | head: Link<T>,
| ^^^^^^^^^^^^^
コンパイラ、しーっ。すぐに使うから。
さて、もう一度 push を書く作業に進みましょう。今回は、挿入後に Option<&mut Node<T>> を取得するのではなく、Box の中身への *mut Node<T> をすぐに取得します。これを健全に行えることはわかっています。なぜなら、Box の中身は、Box 自体を移動しても安定したアドレスを持つからです。もちろん、これは安全ではありません。なぜなら、単に Box を drop してしまえば、解放済みメモリへのポインタを持つことになるからです。
通常のポインタから生ポインタを作るにはどうすればよいでしょうか?型強制です!変数が生ポインタとして宣言されていれば、通常の参照はそこへ型強制されます。
let raw_tail: *mut _ = &mut *new_tail;
必要な情報はすべて揃いました。コードを、おおよそ以前の参照版へと変換できます。
pub fn push(&mut self, elem: T) {
let mut new_tail = Box::new(Node {
elem: elem,
next: None,
});
let raw_tail: *mut _ = &mut *new_tail;
// .is_null は null をチェックし、None をチェックすることと同等です
if !self.tail.is_null() {
// 古い tail が存在していた場合は、新しい tail を指すように更新します
self.tail.next = Some(new_tail);
} else {
// そうでなければ、head がそれを指すように更新します
self.head = Some(new_tail);
}
self.tail = raw_tail;
}
> cargo build
error[E0609]: no field `next` on type `*mut fifth::Node<T>`
--> src/fifth.rs:31:23
|
31 | self.tail.next = Some(new_tail);
| ----------^^^^
| |
| help: `self.tail` is a raw pointer;
| try dereferencing it: `(*self.tail).next`
え?Node へのポインタを持っているのに、なぜ next フィールドを取得できないのでしょうか?
生ポインタを使うとき、Rust はちょっと意地悪です。生ポインタの中身へアクセスするには、手動でそれを deref することを要求します。なぜなら、それは非常に unsafe な操作だからです。では、そうしましょう。
*self.tail.next = Some(new_tail);
> cargo build
error[E0609]: no field `next` on type `*mut fifth::Node<T>`
--> src/fifth.rs:31:23
|
31 | *self.tail.next = Some(new_tail);
| -----------^^^^
| |
| help: `self.tail` is a raw pointer;
| try dereferencing it: `(*self.tail).next`
ううう、演算子の優先順位。
(*self.tail).next = Some(new_tail);
> cargo build
error[E0133]: dereference of raw pointer is unsafe and requires
unsafe function or block
--> src/fifth.rs:31:13
|
31 | (*self.tail).next = Some(new_tail);
| ^^^^^^^^^^^^^^^^^ dereference of raw pointer
|
= note: raw pointers may be NULL, dangling or unaligned;
they can violate aliasing rules and cause data races:
all of these are undefined behavior
こんなに。大変で。あるべきじゃ。ない。
Unsafe Rust は Safe Rust にとっての FFI 言語のようなものだ、と私が言ったのを覚えていますか?さて、コンパイラは、この FFI 的なことを行っている場所を明示的に区切ることを求めています。選択肢は 2 つあります。第一に、関数全体を unsafe としてマークできます。その場合、その関数は Unsafe Rust の関数となり、unsafe コンテキストでのみ呼び出せます。これはあまりよくありません。なぜなら、私たちはリストを安全に使えるようにしたいからです。第二に、関数の中に unsafe ブロックを書いて、FFI 境界を区切ることができます。これにより、関数全体としては安全であると宣言されます。こちらをやりましょう。
pub fn push(&mut self, elem: T) {
let mut new_tail = Box::new(Node {
elem: elem,
next: None,
});
let raw_tail: *mut _ = &mut *new_tail;
if !self.tail.is_null() {
// こんにちはコンパイラ、私は危険なことをしているとわかっています。
// そして、決して間違いを犯さない良いプログラマであると約束します。
unsafe {
(*self.tail).next = Some(new_tail);
}
} else {
self.head = Some(new_tail);
}
self.tail = raw_tail;
}
> cargo build
warning: field is never used: `elem`
--> src/fifth.rs:11:5
|
11 | elem: T,
| ^^^^^^^
|
= note: #[warn(dead_code)] on by default
やった!
今のところ unsafe ブロックを書かなければならなかった場所がそこだけというのは、なかなか興味深いです。あちこちで生ポインタ関連のことをしているのに、どういうことでしょうか?
実は、unsafe に関して Rust はものすごく規則にうるさい杓子定規な存在です。私たちは当然ながら Safe Rust プログラムの集合を最大化したいと考えています。なぜなら、それらは私たちがはるかに強い確信を持てるプログラムだからです。これを実現するために、Rust は unsafety の表面積を最小限に慎重に切り出しています。生ポインタを扱ってきた他のすべての場所では、それらを代入しているか、それらが null かどうかを観察しているだけだったことに注意してください。
生ポインタを実際に dereference しない限り、それらは完全に安全に行えることです。単に整数を読み書きしているだけです!生ポインタで実際に問題に巻き込まれる可能性があるのは、それを実際に dereference した場合だけです。したがって Rust は、その操作だけが unsafe であり、それ以外は完全に安全だと言っているのです。
超。杓子定規。でも技術的には正しい。
語り手: 世界の反対側のどこかで、あるハードウェアエンジニアが 背筋に寒気を覚える — きっとまた誰かがポインタは単なる整数だと 主張しているに違いない。彼女は新しいハードウェアポインタ認証方式の 提案書に目を落とし、一筋の涙を流す。隣の部屋のコンパイラエンジニアは 何も感じない — 彼らはずっと前に、常に厚手のセーターを着ることを 学んでいた。
ポインタ操作の一部だけが実際に unsafe であるという状況は、興味深い問題を引き起こします。unsafe ブロックで unsafety のスコープを区切ることになっているにもかかわらず、実際にはそのブロックの外側で確立された状態に依存しているからです。関数の外側にさえ依存しています!
これを私は unsafe 汚染 と呼んでいます。モジュール内で unsafe を使った瞬間に、そのモジュール全体が unsafety で汚染されます。unsafe コードのすべての不変条件が守られるようにするには、あらゆるものが正しく書かれていなければなりません。
この汚染はプライバシーによって管理可能になります。モジュールの外側から見ると、私たちのすべての構造体フィールドは完全に private なので、他の誰も私たちの状態を勝手な方法でいじることはできません。公開している API のどの組み合わせによっても悪いことが起きない限り、外部の観察者から見れば、私たちのコードはすべて safe なのです!そして実際、これは FFI の場合と何も違いません。ある Python の数学ライブラリが内部で C を呼び出していたとしても、安全なインターフェイスを公開している限り、誰も気にする必要はありません。
ともあれ、pop に進みましょう。これはリファレンス版とほとんどそのまま同じです。
pub fn pop(&mut self) -> Option<T> {
self.head.take().map(|head| {
let head = *head;
self.head = head.next;
if self.head.is_none() {
self.tail = ptr::null_mut();
}
head.elem
})
}
ここでも、safety が状態を持つ別のケースが見られます。この関数で tail ポインタを null にし忘れても、その場ではまったく問題は起きません。しかし、その後の push 呼び出しが、ダングリングした tail に書き込み始めてしまいます!
試してみましょう。
#[cfg(test)]
mod test {
use super::List;
#[test]
fn basics() {
let mut list = List::new();
// 空のリストが正しく振る舞うことを確認
assert_eq!(list.pop(), None);
// リストに要素を追加
list.push(1);
list.push(2);
list.push(3);
// 通常の削除を確認
assert_eq!(list.pop(), Some(1));
assert_eq!(list.pop(), Some(2));
// 何も壊れていないことを確認するために、さらにいくつか push する
list.push(4);
list.push(5);
// 通常の削除を確認
assert_eq!(list.pop(), Some(3));
assert_eq!(list.pop(), Some(4));
// 要素を取り尽くす場合を確認
assert_eq!(list.pop(), Some(5));
assert_eq!(list.pop(), None);
// 取り尽くした場合にポインタが正しく修正されたことを確認
list.push(6);
list.push(7);
// 通常の削除を確認
assert_eq!(list.pop(), Some(6));
assert_eq!(list.pop(), Some(7));
assert_eq!(list.pop(), None);
}
}
これは単にスタックのテストで、期待される pop の結果を逆向きにしたものです。また、pop における tail ポインタ破損のケースが発生しないことを確認するため、最後に追加の手順も入れています。
cargo test
running 12 tests
test fifth::test::basics ... ok
test first::test::basics ... ok
test fourth::test::basics ... ok
test fourth::test::peek ... ok
test second::test::basics ... ok
test fourth::test::into_iter ... ok
test second::test::into_iter ... ok
test second::test::iter ... ok
test second::test::iter_mut ... ok
test second::test::peek ... ok
test third::test::basics ... ok
test third::test::iter ... ok
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured
金の星です!
語り手: 来るぞ…