Rustをはじめよう その6(所有権・移動編)

三菱総研DCS デジタル企画推進部の加藤です。 この記事では、プログラミング言語Rust(ラスト)の所有権の移動について、その概要をまとめます。

前書き

記事概要

  • この記事ではRustにおける所有権の移動についてご説明します。
  • 前回(所有権・仕組み)前々回(所有権・前提知識)の記事との関連が深い内容です。これら2つの記事をまだお読みでない方は、内容を確認した上で本記事をお読みになることをおすすめします。

移動

前回の記事で、(値の所有権の)移動について簡単に紹介しました。このセクションでは、移動が発生する各パターンについて、より詳しく見ていきます。

変数への値の代入

このパターンは、前回の記事で見たとおりです。例えば、以下のサンプルコードでは、vector1からvector2へ所有権が移動します。

let mut vector1 = Vec::new();
vector1.push(String::from("a"));
vector1.push(String::from("b"));
vector1.push(String::from("c"));

let vector2 = vector1;


関数に対する引数(値)の受け渡し

関数の引数に値を渡すと、それに伴って所有権が移動します。
以下のサンプルコードでは、ユーザ定義関数funcに対して、変数vectorを引数として値渡しします。その後、for文でvectorの内容を出力しようとしています。

main.rs

fn main() {
    let mut vector = Vec::new();
    vector.push(String::from("a"));
    vector.push(String::from("b"));
    vector.push(String::from("c"));

    func(vector);

    for str in vector {
        println!("{}", str);
    }
}

fn func(vec: Vec<String>) {
    // 任意の処理
}


出力結果

error[E0382]: use of moved value: `vector`
 --> src\main.rs:9:16
  |
2 |     let mut vector = Vec::new();
  |         ---------- move occurs because `vector` has type `std::vec::Vec`, which does not implement the `Copy` trait
...
7 |     func(vector);
  |          ------ value moved here
8 | 
9 |     for str in vector {
  |                ^^^^^^ value used here after move


このコードを実行すると、エラーが発生してコンパイルが失敗します。エラーメッセージは、以前のサンプルコードで所有権が移動して未初期化状態の変数にアクセスした場合と同様に、コードの7行目(関数に変数を値渡しする行)で移動が発生し、9行目で移動後の値が使用されていることを指摘しています。

関数の引数に値渡しをすると所有権は移動しますが、この挙動は処理を実装する上で都合が悪い場合があります。上記のサンプルコードでも、ユーザ定義関数funcの実行後にvectorにアクセスできなくなってしまっており、このような処理は実装できないことになります。さらに、関数funcの引数vecに対してvectorから所有権が移動していますが(サンプルコードの7行目)、funcの処理が終わった時点でvecはスコープを抜けて破棄されるため(サンプルコードの16行目)、サンプルコードの2行目から5行目の間にvectorが所有していたVec型の値(実データ)もろとも消滅してしまいます。せっかく生成したデータが、関数に引数として渡すと消えてしまうわけです。
もちろん、関数funcの戻り値を工夫して、例えば引数vecで受け取った値をそのまま呼び出し元へ返却すれば、呼び出し元で再び値を使用することはできます。ただ、この実装の仕方はあまりに煩雑です。
サンプルコードの関数(の引数)のように、所有権そのものは不要だが、値に対してアクセスしたい場合に用いられるのが 参照 という機能です。参照は、所有権と同じくRustにおいて非常に重要な機能のため、次回の記事にて詳しく解説します。

関数からの戻り値の受け取り

関数から値が戻り値として呼び出し元に返却される場合も、所有権の移動が発生します。
例として、直前で使用したサンプルコードを少し変更し、関数funcから引数vecをそのまま返却するように修正します。また、関数の戻り値は変数vectorで受け取り、for文でvectorの内容を出力します。

main.rs

fn main() {
    let mut vector = Vec::new();
    vector.push(String::from("a"));
    vector.push(String::from("b"));
    vector.push(String::from("c"));

    vector = func(vector);

    for str in vector {
        println!("{}", str);
    }
}

fn func(vec: Vec<String>) -> Vec<String> {
    // 何かの処理
    vec
}


出力結果

a
b
c


実行結果について解説する前に、Rustにおける関数の実装の仕方についてごく簡単に紹介します。
fn func(vec: Vec<String>)の後ろの-> Vec<String>は、関数が->の右側の型の戻り値を返却することを表します。また、Rustの関数は、{}内に書かれた処理の末尾に;(セミコロン)なしで書かれた値をこの関数の戻り値とみなします。多くのプログラミング言語では戻り値を返却するためにreturn命令を明示的に書く必要がありますが、Rustでは不要です(Rustのreturnは早期リターンする場合に使用します)。

実行結果の解説に戻ります。コードを実行すると、コンパイルが成功してvectorの内容がコンソール出力されます。このことから、変数vectorから関数funcの引数vecに移動した所有権が、funcの戻り値によってvectorに再度移動していることがわかります。

制御フローと移動

ここまで、移動が発生する事例をいくつか紹介しました。最後に、これらの移動のパターンがプログラム中の制御フローと組み合わさった場合に、どのような挙動を取るのかを確認しましょう。

以下のサンプルコードでは、これまでのサンプルコードと同様にVec型の値を宣言しています。その後、if文による条件分岐がありますが、判定式にはtrueを入れているため、実行時の処理としては何もしないことになります。ただし、else{}では引数にVec型の値を取る関数funcを実行することになっています。このif文の後、同じく関数funcを実行しています。
変数vectorが所有権を持った状態で関数funcを実行することになるので、所有権とその移動を考慮してもプログラムの実行に問題はないように見えますが、実行結果はどのようになるでしょうか。

main.rs

fn main() {
    let mut vector = Vec::new();
    vector.push(String::from("a"));
    vector.push(String::from("b"));
    vector.push(String::from("c"));

    if (true) {
        // 何もしない
    } else {
        func(vector);
    }
    func(vector);
}

fn func(vec: Vec<String>) {
    for str in vec {
        println!("{}", str);
    }
}


出力結果

error[E0382]: use of moved value: `vector`
  --> src\main.rs:12:10
   |
2  |     let mut vector = Vec::new();
   |         ---------- move occurs because `vector` has type `std::vec::Vec<std::string::String>`, which does not implement the `Copy` trait
...
10 |         func(vector);
   |              ------ value moved here
11 |     }
12 |     func(vector);
   |          ^^^^^^ value used here after move


error: aborting due to previous error


サンプルコードを実行すると、コンパイルが失敗します。エラーメッセージを見ると、10行目でelse{}内の関数funcの実行箇所において変数vectorからfuncの引数への移動が発生し、12行目(else{}の後)の関数funcの実行箇所で移動後の値が使用されている旨が指摘されています。

次に、for文で関数funcを繰り返し実行する場合を見ていきます。以下のサンプルコードは、funcを10回実行することを意図しています。

main.rs

fn main() {
    let mut vector = Vec::new();
    vector.push(String::from("a"));
    vector.push(String::from("b"));
    vector.push(String::from("c"));

    for number in 0..10 {
        func(vector);
    }
}

fn func(vec: Vec<String>) {
    for str in vec {
        println!("{}", str);
    }
}


出力結果

error[E0382]: use of moved value: `vector`
 --> src\main.rs:8:14
  |
2 |     let mut vector = Vec::new();
  |         ---------- move occurs because `vector` has type `std::vec::Vec<std::string::String>`, which does not implement the `Copy` trait
...
8 |         func(vector);
  |              ^^^^^^ value moved here, in previous iteration of loop


error: aborting due to previous error


サンプルコードを実行すると、if文の場合と同様にコンパイルが失敗します。ただし、エラーメッセージはif文の場合と異なり、8行目で前回のループで移動済みの値が使用されている旨が指摘されています。これは、for文における初回のループ処理で変数vectorから関数funcの引数へ所有権が移動したため、2回目以降のループ処理ではvectorが未初期化となっていて使用できないことを表しています。

以上のサンプルコードの結果から分かる通り、プログラム実行時に実際には移動が発生しない場合も含めて、if文のいずれかの分岐先やfor文のループ処理内で変数から所有権の移動が発生する場合、コンパイラはその変数から所有権が移動するものと判断します。 そのため、後続の処理ではその変数が所有していた値にアクセスすることはできません。アクセスした場合はエラーとなり、コンパイルは失敗します。
このようにコンパイラが判断することによって、ダングリングポインタの使用(解放済みのメモリ領域を参照するポインタ)によって発生するバグをコンパイルの段階で防いでいます。さらに、ある分岐では移動が発生するためエラーとなるが、別の分岐では移動が発生せずエラーとならない、といった条件に依存したバグの発生も未然に防ぐことができます。

まとめ

本記事では、Rust の所有権の移動について、その概要をご紹介しました。
次回は、所有権と関係の深い機能である「参照」について解説する予定です。

参考文献

  • 「The Rust Programming Language: 2nd Edition」, https://doc.rust-jp.rs/book-ja-pdf/book.pdf 2020 年 2 月 26 日アクセス
  • Jim Blandy, Jason Orendorff 著, 中田 秀基 訳(2018)『プログラミングRust』 オライリー・ジャパン
  • κeen,河野 達也,小松礼人(2019)『実践 Rust 入門[言語仕様から開発手法まで]』 技術評論社
その5 |  その6 | その7