13
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

&mutが一つしか作れないことに納得できないRustaceanへ

Last updated at Posted at 2021-03-23

ボローチェッカーとの闘い

Rust はボローチェッカーと戦う言語だといわれることがありますが、その戦いのゴングは次のようなコードで初めて鳴るのではないでしょうか。

    let mut x = 1;
    
    let r = &x;
    
    x = 2;
    
    println!("{}", r);

このようなコードは次のようなエラーで怒られます1

error[E0506]: cannot assign to `x` because it is borrowed
 --> src/main.rs:6:5
  |
4 |     let r = &x;
  |             -- borrow of `x` occurs here
5 |     
6 |     x = 2;
  |     ^^^^^ assignment to borrowed `x` occurs here
7 |     
8 |     println!("{}", r);
  |                    - borrow later used here

error: aborting due to previous error; 1 warning emitted

他の言語にはこのようなルールは無いので、初めて出くわしたときは面食らう人が多いと思います。

The book には、このルールは次のように書かれています。

  • 任意のタイミングで、一つの可変参照か不変な参照いくつでものどちらかを行える。

この文章は微妙にわかりにくいと思いますが、要は下記のテーブルのように、 & は複数持てる、 &mut は一つだけ持てる、ただし & と &mut は同時には持てない、ということです。

& &mut
OK 2 0
OK 0 1
ダメ 1 1
ダメ 0 2

また、このルールは「一つのスコープの中で」適用されるので、関数呼び出しなどを通して新たなスコープを導入すれば、可変な参照が同時に2つ以上存在することは可能です。

fn f(x: &mut i32) -> i32 {
    println!("{}", x);
    *x
}
fn main() {
    let mut x = 1;
    let y = &mut x;
    f(y);
    println!("{}", x);
}

まぁ、そういうルールがあるってのはわかるけど、それがデータ安全性にどう役立つの?というところは The Book にも突っ込んで書かれていないので、もやもやする人も多いと思います。 RAII のように C++ でも馴染みのある機構でもないのでなおさらです。

Aliasing

可変な借用が複数あるとまずいのは、aliasingの問題のためです。aliasingとは同じ変数を複数の名前でアクセスすることです。たとえば

fn f(a: &mut i32, b: &mut i32) {
    *a = 1;
    *b = 2;
    *b = *a;
}

という関数は、 *a*b に最終的に 1 が代入されるので、*b = 2 は最適化で除去することができるように見えます。しかし呼び出し元が下記のように同じ変数への参照を ab に渡していた場合、実際には a に 2 が代入されるロジックになるので、最適化によってロジックが変わってしまいます。

    let mut a = 0;
    f(&mut a, &mut a);

このため、CおよびC++ではポインタや参照が同じ型を持っていた場合、同じ実体を参照している可能性があるとみなして最適化を制限します。これをstrict aliasingといい、FortranがCより速いといわれることがある理由です。Rustではこのように同じ実体への複数の参照を作ること自体ができませんので、オプティマイザはaliasingの可能性を考慮せずに最適化できます。

また、aliasingしている型がenum型だった場合はさらに深刻なことが起きます。

fn g(a: &Option<i32>, b: &mut Option<i32>) {
    if let Some(ref v) = a {
        *b = None;
        println!("{}", v);
    }
}

この関数ではRustの標準データ型である Option を使っていますが、 *aSome だったときに *bNone を代入しています。しかしもし呼び出し元が同じ実体への参照を渡すと、

    let mut c = Some(1);
    g(&c, &mut c);

v という変数は Some の中身を参照しているので、他の参照を介して None に変更されてしまうと、無効なバリアントを参照することになります。これは恐るべき未定義動作で、プログラムがどんな動作をしてもおかしくありません。この例では不変な参照1個と、可変な参照1個で未定義動作が生じてしまいます。このためRustでは可変な参照があるときは不変な参照は1つも作れません。

まとめ

このようにRustの借用のルールは、データの整合性を静的に保証するために必要なものです。もしこのルールがなければ、穴のある保証になってしまいます。理由が分かれば、ボローチェッカーは戦う相手ではなく、安心して背中を任せられる戦友となるのではないでしょうか。

なお、ボローチェッカーは静的なチェック機構ですが、たまに実行時に参照のルールをチェックする必要が生じます。木構造やリファレンスカウンタ式のスマートポインタなどです。この場合には RefCell が使われます。よく見られるのは Rc<RefCell<Type>> といったような型です。

よくある誤解

データの整合性の保証には、必ずしもマルチスレッドが関わっているわけではありません。ボローチェッカーのルールはあくまでもシングルスレッドの aliasing の問題のために作られたものです。マルチスレッドプログラミングでの競合を避けるのにも役立つというのは、後になって発見されたことです。

また、ボローチェッカーだけをオフにできないかというのもたまに聞かれますが、ボローチェッカーはデータの整合性を保証するために必要なものであり、単独でOn/Offできるようなものではありません。ボローチェッカーがなければ strict aliasing の対策などを導入する必要が出てきて、言語設計が大幅に影響を受けてしまいます2。これは Rust 言語の根幹に結びついており、言語の一部となっているといっていいでしょう。

私も最初にこのルールを見たときは納得がいきませんでした。"The book"は良く書かれたドキュメントですが、この点についてもう少し掘り下げて説明してほしかったと思います。

  1. 最後の println! を削除すれば、 2018 Edition ではコンパイルできます。 Non-lexical Lifetime と呼ばれる機能によるものです。

  2. unsafe ブロックを使えばボローチェッカーもろとも静的チェックをすべて無効にできますが、これは生の C++ よりも危険なコードです。

13
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?