26
15

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.

C++の参照とRustの参照

Last updated at Posted at 2020-08-01

C++の参照 T& と Rustの参照 &'a T は割と使い勝手が違うよという話

C++の参照

#include <iostream>
#include <vector>

class A {
    std::string name;
public:
    A(std::string name): name(name) {}
    const std::string& get_name() const {
        return this->name;
    }
};

int main() {
    A* a = new A("a");
    auto& name_ref = a->get_name();
    delete a;
    
    std::cout << name_ref << std::endl;
    return 0;
}

C++でこの様に構造体内のオブジェクトの参照を返すと、先にオブジェクトが解放されても依然参照にアクセス出来てしまうので簡単にバグってしまいます。なのでC++ユーザーはこのようなコードは普段避けているはずで、例えば値を複製して返したりしているはずです。

Rustの参照

一方Rustではこの様なことが起こらないようになっています

struct A {
  name: String,
}

impl A {
    fn new(name: String) -> Self {
        A { name }
    }
    
    fn get_name(&self) -> &str {
        &self.name
    }
}

fn main() {
    let a = A::new("a".into());
    let name = a.get_name();
    drop(a);
    println!("{}", name);
}

同じように構造体 A の内部のメンバへの参照を返して、その後に参照へアクセスしようとするコードですが、これはコンパイルエラーになります:

error[E0505]: cannot move out of `a` because it is borrowed
  --> src/main.rs:18:10
   |
17 |     let name = a.get_name();
   |                - borrow of `a` occurs here
18 |     drop(a);
   |          ^ move out of `a` occurs here
19 |     println!("{}", name);
   |                    ---- borrow later used here

error: aborting due to previous error

これは省略されているlifetimeを表示すると少しわかりやすくなるでしょう:

    fn get_name<'life>(&'life self) -> &'life str {
        &self.name
    }

このように明示的に書くことも出来ますが、いくつか省略するルールがあり良く使う場合はあまり明示的に書かなくてもいいようになっています。これは雑に言えば「selfを借りた時の生存時間 'life で参照 &'life str を作れ」という事です。参照にいつまで有効かどうかの情報が型レベルで入っているわけですね。

なので main のコードは get_name を読んだとき、まず a がいつまで借りることができるかを判断して、その生存期間で name の型を決めようとします。大事な事ですが、これはスクリプト言語のように、上から順番に行を評価をするだけでは決まりません。

一見 a を借りることができるのは次の drop(a) が起こるまでの様に見えます:

fn main() {
    let a = A::new("a".into()); // ここから
    let name = a.get_name();
    drop(a);                    // ここまで?
    println!("{}", name);
}

なのでこの範囲でしか生きれない生存時間 'life を使って参照 name が定義されるのでしょうか?すると次の println!name を使おうとしたとき、この name は型で定められた生存範囲より外で使われているのでエラーになります。

これではダメなのでコンパイラは name が使われる範囲も含めて 'life にしようとします

fn main() {
    let a = A::new("a".into()); // ここから
    let name = a.get_name();
    drop(a);
    println!("{}", name);       // ここまで使えないとまずい
}

しかしそうすると drop(a) と矛盾してしまいます。ここで諦めてコンパイラは上の様にエラーを吐いているわけです。

まとめ

このようにRustのコンパイラは参照についている生存期間まで含めて推論してくれています。このおかげでC++では危険だった参照の使い方が安全になるため、参照でなく値を返す事を避ける事ができる場合もあります。

26
15
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
26
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?