[Rust] AtCoderをやる Rustを褒める C++と比べる


目標

Rustの良いところを他の言語 (とくに C++) と比較して考察しながら、AtCoderに慣れていく

C++ の問題点

RustもC++と同様にスマートポインタやコンテナがあるが、所有権や借用の概念により、それはつまり、自分でメモリの確保と解放を行わなくて良いということになる

他言語にはガベージコレクタという機能を持つものが存在し、メモリを自動で管理してくれて便利なこともある

ガベージコレクタはとても賢いが、万能ではない
なぜなら、 ほとんどのデータをヒープに (メモリ領域が明確になるスタックと違い、ただ漠然とメモリ領域を確保する) に置き参照経由で扱わなければならず、 実行速度にも多少影響する

また、C++のスマートポインタは不正な状態のポインタの使用を完全に防ぐことはできない

特にイテレーションはコレクションに対する走査を行うための標準的なツールにもかかわらず実質的に単なるポインタと同様なので、しばしば無効なイテレータが発生する

C++ より Rust を学ぶことを推奨

イテレータの無効化

イテレート中に要素の追加などのイテレータを無効化する操作を行うと実行時エラーが発生する

配列外参照により segmentation fault となることもある (ローンチ後にこのエラーになると面倒)
これらが無駄なデバッグ時間をつくる

Rust ではイテレータを無効化する操作はコンパイルエラーになるため、誤ったコードを書いてもすぐ気づける

暗黙の型変換

longコンテナの全要素の総和をしようと、accumulate関数で総和を計算しようとすると、関数が暗黙の型変換をしてintで総和を計算しようとしてオーバーフローになる

#include <iostream>
#include <limits>
#include <numeric>
#include <vector>

int main() {
    // long long で表せる最大値を`large`とする。
    long long large = std::numeric_limits<long long>::max();

    std::vector<long long> s = {large};

    // sには`large`しか入っていないので、総和は普通に`large`になるはず。ところが
    // 総和の型をint型だと思って計算してしまいオーバーフローを起こしてしまう。
    // コンパイルエラーも実行時エラーも起こらない!
    std::cout << std::accumulate(s.begin(), s.end(), 0) << std::endl;

    // 正しくは以下の通り。
    std::cout << std::accumulate(s.begin(), s.end(), 0LL) << std::endl;
}

Rust は整数の暗黙の型変換を認めていないので、型の一致でコンパイルエラーになる

let s = vec![std::i64::MAX];
let x: i32 = s.into_iter().sum();
// E0277: the trait bound `i32: std::iter::Sum<i64>` is not satisfied
// 3 | let x: i32 = s.into_iter().sum();
//   |                            ^^^ the trait `std::iter::Sum<i64>` is not implemented for `i32`

Rust コンパイラのエラーメッセージは親切

C++ のテンプレートでは、テンプレート引数が不正なとき、実体化のタイミングでエラーが発生する
そのためエラーメッセージが大量に発生することがある
ぎっちぎちで読みにくいし、ヒントも親切とはいえない

Rust には、コンパイル駆動設計という言葉があるぐらいにコンパイルエラーが読みやすい

エラーが関連する他の場所や修正方法のヒントなどをわかりやすく表示してくれる

トレイトやジェネリクスの仕組みによりエラーの所在が明確化されているので、コンパイル時にエラーの存在を教えてくれる

リンク集

AtCoderで使用できるクレート最新情報
プログラミング言語Rust入門
実践Rust入門
C++入門