Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
16
Help us understand the problem. What is going on with this article?
@termoshtt

C++の参照とRustの参照

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++では危険だった参照の使い方が安全になるため、参照でなく値を返す事を避ける事ができる場合もあります。

16
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ricos
FEMによる構造解析、機械学習の専門家集団。計算資源のクラウド提供もしています。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
16
Help us understand the problem. What is going on with this article?