これは何?
Rustの安全性についてよく誤解されているようなので、より良く理解できるようにC++との比較を私なりに行ってみました。
C++の場合
プログラミング初心者である私は、C++で文書を管理する Document
クラスを書いた。
このクラスは、コンストラクターで3つの段落を生成し、 paragraphs
というメンバーに格納する。
#include <string>
#include <vector>
#include <iostream>
// 文書を表すクラス
class Document {
public:
Document() {
// 3つの段落を追加
paragraphs.push_back("paragraph 1");
paragraphs.push_back("paragraph 2");
paragraphs.push_back("paragraph 3");
}
// 指定された段落の文字列への参照を返す
const std::string& get_paragraph(int i) const {
return paragraphs[i];
}
public:
std::vector<std::string> paragraphs;
};
int main() {
// Documentを作る
const Document doc;
// 0番目の段落の文字列への参照を取得
const std::string& p = doc.get_paragraph(0);
// 段落の文字列を表示
std::cout << p << std::endl;
return 0;
}
これを実行すると、
$ g++ ./cpp_code.cc -O3 -o cpp_code
$ ./cpp_code
paragraph 1
期待通り、0番目の段落の文字列が表示された。
次に、文書を変更することを考えた。
私はプログラミング初心者なので、以下のような誤ったコードを書いてしまった。
int main() {
// Documentを作る。後で変更を加えるので、constを外した。
Document doc;
// 0番目の段落の文字列への参照を取得
const std::string& p = doc.get_paragraph(0);
// 段落をすべて消去
doc.paragraphs.clear();
doc.paragraphs.shrink_to_fit();
// 新しい段落を追加
doc.paragraphs.push_back("new paragraph");
// 段落の文字列を表示
std::cout << p << std::endl;
return 0;
}
これを実行したところ、コンソールには大量の空白が延々と出力され続けた。
原因は、 p
の参照先が既に削除されてしまっており、不正なアドレスにアクセスしているためだ。
CやC++では上のような明らかに誤ったコードもコンパイルできてしまうため、プログラマーには常に大きな責任が要求される。
プログラミング初心者にとっては難易度が高そうだ・・・
上級者であっても、常に間違えずに書ける保証はない。
Rustの場合
Rustでは、複数の不変参照、または1つの可変参照のどちらかしか同時に存在できないという制約を置くことで、このようなバグを排除する。
まずは、一番最初のC++の例をRustで書いたものを示す。
/// 文書を表す構造体
struct Document {
paragraphs: Vec<String>,
}
impl Document {
fn new() -> Self {
// 3つの段落を追加
Self {
paragraphs: vec![
"paragraph 1".to_string(),
"paragraph 2".to_string(),
"paragraph 3".to_string(),
],
}
}
/// 指定された段落の文字列への参照を返す
fn get_paragraph(&self, i: usize) -> &str {
&self.paragraphs[i]
}
}
fn main() {
// Documentを作る
let doc = Document::new();
// 0番目の段落の文字列への参照を取得
let p = doc.get_paragraph(0);
// 段落の文字列を表示
println!("{p}");
}
$ cargo run --release
paragraph 1
次に、C++のときと同様に、文書の変更を試みる。
fn main() {
// Documentを作る。後で変更を加えるので、mut変数にする。
let mut doc = Document::new();
// 0番目の段落の文字列への参照を取得
let p = doc.get_paragraph(0);
// 段落をすべて消去
doc.paragraphs.clear();
// 新しい段落を追加
doc.paragraphs.push("new paragraph".to_string());
// 段落の文字列を表示
println!("{p}");
}
ところが、このコードは以下のようなエラーメッセージが出てコンパイルできない。
error[E0502]: cannot borrow `doc.paragraphs` as mutable because it is also borrowed as immutable
--> src/main.rs:31:5
|
29 | let p = doc.get_paragraph(0);
| -------------------- immutable borrow occurs here
30 |
31 | doc.paragraphs.clear();
| ^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
...
35 | println!("{p}");
| - immutable borrow later used here
エラーメッセージによると、get_paragraph()
の戻り値である不変参照 p
は doc
を不変借用しており、p
が存在する限り doc
を可変借用する必要のある処理はできないということだ。
文書を変更するには、文字列への参照を取得する前に行えば良い。
fn main() {
let mut doc = Document::new();
doc.paragraphs.clear();
doc.paragraphs.push("new paragraph".to_string());
let p = doc.get_paragraph(0);
println!("{p}");
}
$ cargo run --release
new paragraph
Rustコンパイラーは、プログラミング初心者の私が誤ったコードを書いてしまうリスクを軽減してくれる、頼もしい存在だ。
おわりに
Rustのメモリ安全性を保証する仕組みは他にもありますが、その一端を紹介しました。
さらに理解するには、所有権やライフタイムなどの概念についても触れる必要があります。詳しくはRustのドキュメントに記載されているので、そちらをお読みください。