Rustのメモリ安全について


目的

Rustのメモリ安全を実現している機能について知る。

まとめ

所有権と参照、借用により実現されている。
所有権:値の所有者は必ず一つに限られる。
→適切なタイミングでの解放、二重解放の防止。
参照:所有権が移って都度解放されていては使いにくいため導入された仕組み。
→C++に似ている。デフォルトがconst参照。可変参照の制約がある点が異なる。

メモリ安全性の保証

Rustは他の言語とは異なるメモリの管理の方法をとっている。
①CやC++は明示的にメモリ確保、破棄を行う必要がある。
メモリリークやNullアクセスが発生し、危険。
②pythonやGoなどはプログラミング側が管理してくれるが、ガベッジコレクションが行われるため速度が遅くなる。
③Rustはコンパイラがコンパイル時にチェックする一定の規則(所有権、借用、スライス)で管理。かつ所有権システムは動作を遅くしない。

所有権規則

①Rustの各値は、所有者と呼ばれる変数と対応している。
②いかなる時も所有者は一つである。
③所有者がスコープから外れたら、値は破棄される。

スコープとdrop

ルール③を実現するための機能。
下記はスコープを外れるとdropにより破棄されている例。
dropはスコープを抜けた際にメモリを返還してくれる関数。

qiita.rs

{
    let s = String::from("hello"); // sはここから有効になる

    // sで作業をする
}                                  // dropが呼ばれsは解放される

C++であればRAIIで書く必要がある。

qiita.cpp

void main() {
    {
        unique_ptr<string>a(new string("Hello"));
        cout << *a;//hello
    }//デストラクタで解放される

}                      

ムーブ

ルール②を実現するための機能

C/C++の問題点

Deep Copyが実行された場合メモリの使用量が2倍になる。コピーするため実行速度が遅い。
ヒープ上のhellowはコピーによりもう一か所にも作成され、メモリ使用量は2倍となる。

qiita.cpp

void main() {

    string s1 = "hello";
    string s2 = s1;//Deep Copy

}                 

Rustの選択:ムーブ。いかなる時も所有者は一つである。

s2=s1のコピーはデータのコピーはせず、ptrとlen,capacity(確保したメモリサイズ)のコピーのみ行われる(shallow copy)
これにより、実行速度が悪くならない。また、解放時の二重解放も起きない。
Deep Copyしたい場合はs1.clone()で可能。

moveで関数に渡されたものは関数内で解放される。
解放後に使用するようなコードを書くとコンパイルエラーになる。

qiita.rs

fn main() {
    let s = String::from("hello");  // sがスコープに入る

    takes_ownership(s);             // sの値が関数にムーブされ...
                                    // ... ここではもう有効ではない

   // println!("{}", s);//sはtakes_ownership(s); の中で破棄されているためコンパイルエラー

    let x = 5;                      // xがスコープに入る

    makes_copy(x);                  // xも関数にムーブされるが、
                                    // i32はCopyなので、この後にxを使っても
                                    // 大丈夫

} // ここでxがスコープを抜け、sもスコープを抜ける。ただし、sの値はムーブされているので、何も特別なことは起こらない。
  //

fn takes_ownership(some_string: String) { // some_stringがスコープに入る。
    println!("{}", some_string);
} // ここでsome_stringがスコープを抜け、`drop`が呼ばれる。後ろ盾してたメモリが解放される。
  // 

fn makes_copy(some_integer: i32) { // some_integerがスコープに入る
    println!("{}", some_integer);
} // ここでsome_integerがスコープを抜ける。何も特別なことはない。               

Copyされるもの

下記のようなコードではxは解放されない。

qiita.cpp

let x = 5;
let y = x;//xは解放されない

println!("x = {}, y = {}", x, y);                    

解放されないもの→スカラ値の集合
解放されるもの→メモリ確保が必要だったりするもの。

以下のものは解放されない。

・あらゆる整数型。u32など。
・論理値型であるbool。trueとfalseという値がある。
・あらゆる浮動小数点型、f64など。
・文字型であるchar。
・タプル。ただ、Copyの型だけを含む場合。例えば、(i32, i32)はCopyだが、 (i32, String)は違う。

参照

関数の外で使用するために所有権を毎回返すのは面倒くさい。
この問題を解決するのが参照と借用.

qiita.rs

fn foo(v1: Vec<i32>, v2: Vec<i32>) -> (Vec<i32>, Vec<i32>, i32) {
    // v1とv2についての作業を行う

    // 所有権と関数の結果を返す
    (v1, v2, 42)
}

let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];

let (v1, v2, answer) = foo(v1, v2);          

&記号を付けると参照になる。関数の引数に参照を取ることを借用と呼ぶ。
C++と同じ。
calculate_length()はs1を借用しているだけで、所有権をもっていないのでs1を解放することはない。
C++と異なる点は、デフォルトがconst参照渡しになっている点。(Effective C++20項)
参照している変数を変更しようとするとコンパイルエラーになる。(GOOD!)

qiita.rs

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    // '{}'の長さは、{}です
    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}   

参照している変数を変更したいときは&mutをつける。

qiita.rs

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

可変参照の制約

特定のスコープで、ある特定のデータに対しては、一つしか可変な参照を持てない。
sに対する可変参照が2か所存在するためコンパイルエラーとなる。(GOOD!)
この制約のおかげでデータの競合を防ぐことができる。
不変な参照であれあばOK。

qiita.rs

let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

データの競合は以下の3つの振る舞いが起きるときに発生する

①2つ以上のポインタが同じデータに同時にアクセスする。
②少なくとも一つのポインタがデータに書き込みを行っている。
③データへのアクセスを同期する機構が使用されていない

C++では同じデータへの参照を複数個所で使用することができ、同時にアクセスされる危険性がある。

qiita.cpp

void main() {

    int a = 3;

    int& b = a;
    int& c = a;

}