RustとC++のジェネリクスの性格の差

今までRustは使いまわすことのない適当なスクリプト的にしか使ってこなかったので、実際のところ本質的に難しいことは何もしてこなかった。その間は非常に楽で、言われるほど難しくないのではと思っていた。が、最近コードを使いまわそうと思って書き始めたところ、即死してしまった。

例えば、ジェネリックな構造体を作ろう。名前と座標を持つ質点ということにしよう。すると、名前は文字列でいいとして、座標にはf64f32のどちらも使い得るし、そもそも座標の値は特定の型に結びついたものではない。なのでジェネリクスを使うのが妥当だ。なので以下のようなコードを書き始める。

#[derive(Debug)]
pub struct Particle<T> {
    pub name : std::string::String,
    pub pos  : nalgebra::Vector3<T>,
}

fn main() {
    let p = Particle::<f64> {
        name: "C".to_string(),
        pos: nalgebra::Vector3::new(1.0, 1.0, 1.0)
    };
    println!("{}", p);
}

これでもうだめだ。C++のtemplate的な思考で行くと、Tf64が入り、その結果Particle<f64>が実体化され、結果、型の代入が成功するので問題なく動く。と予想される。

だがRustはそれを許さない。例えば以下のようなエラーがでる。

error[E0277]: the trait bound `T: std::marker::Copy` is not satisfied                                                                                                                          
 --> src/main.rs:3:5                                                                                                                                                                           
  |                                                                                                                                                                                            
3 |     pub pos  : nalgebra::Vector3<T>,                                                                                                                                                       
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::marker::Copy` is not implemented for `T`                                                                                               
  |   

これは、nalgebra::Vector3Tに対してstd::marker::Copyトレイトを実装していることを要求していることに起因している(同期曰く「Rustでは構造体の型パラメータにトレイト境界を入れるのは非推奨のはずだが……」と言うことらしいのでライブラリ側が微妙な実装をしているだけと言う可能性はある)。ここでTCopyトレイトを実装していない型が代入される可能性があるということをRustコンパイラは指摘している。

使っているのはf64で、当然Copyトレイトを持っているだろう? と思うC++脳の人たちには、これがRustであると言うことを思い出してもらわなければならない。C++のテンプレートはコードジェネレータなので、実際にジェネレートされたコードが正しければ何の問題もない。だが、Rustのジェネリクスは、今のところ代入されていない型に対してもエラーを出し得る。「あとで誰かがTにコピー不能型を入れたらどうなる? Tnalgebra::Vector3に入れられ、そこでCopyが要求されているので失敗することになる。つまり、このコードは壊れているということだ!」

そんなもん金輪際入れねえよ、という心の声は飲み込んで、std::marker::Copyをトレイト境界に追加してやらねばならない。確かに、人は最初の設計を忘れる。いずれ、当初は想像もしていなかった謎の何かを型パラメータにぶち込むだろう。それは使い方を知らない他人かもしれないし、未来の自分かもしれない。そうなってからでは遅い、というのがRustの言い分だ。今のうちに考えられるバグの芽は全て潰しておくのがRust流ということだ。

確かにC++だと、あとで奇妙な型を入れた時に、型パラメータの代入途中で出たあらゆるエラーが報告され、エラーメッセージが膨れ上がって大変なことになる。Rustはそういうことが起きる前に先にエラーメッセージを大変なことにしておいてくれるというわけだ。Rustの方がエラーがわかりやすい分(トレイト境界について知っていて、いくつかの頻出トレイトについての知識があり、落ち着いて考えてあるいは経験から上のような話に考えが至ればだが)多少ましという意見は間違いなくある。が、まあとりあえず動くという状態に持っていきにくいので学習に鉄の意志が必要なのも確かだ。

ちなみにこのコードでは、Copyトレイトを追加しても指摘は終わらない。最終的に、nalgebra::Vector3が要求するトレイト境界(nalgebra::base::Scalar)をそのまま持ってくる必要があった。ライブラリで定義されているジェネリクス構造体を中に持つジェネリクス構造体を作る時は、先にライブラリ側のトレイトを確認するのが一番の近道っぽい。