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