きっかけ
このツイートが流れてきたのがきっかけでした。もともとRustには多少なりとも興味があったので
せっかくだから読んで学んでみよう、ということで、プログラミング Rustの第2版を購入して読むことに。
本を読んだ感想
基礎の基礎から順を追って学習するといった内容ではなく、サンプルコードやRustの機能の説明をステップバイステップで行っていき、各章の最後に一緒にコードを書いて何か作る、といった方式で
せっかちな自分にとっては有難い構成でした。
特に、「C++ではこんな事が起きるけど、Rustではこんなことは起きない」といった、C++との比較を通してRustの特色を説明している点が非常にわかりやすかったです。また、プログラミング言語の歴史や背景にも触れながら、Rustが他の言語では解決できなかった課題をどのように克服してきたのかが理解できる、素晴らしい一冊でした。
最初は思想強めだな、なんて思ったりもしたのですが、読んでいくうちに私の思想も見事にRust色に染まっていってしまいました(笑)
Rustの評価
素晴らしいけど難しい言語だと思いました。
素晴らしいと思ったところ
・プロジェクトのセットアップ/環境構築が簡単
rustupをインストールして
cargo new <プロジェクト名>
で終わりである。何日間も費やす必要はない
・マクロが神がかっている
まさかの再帰可能とかいうとんでもない性能
しかも強力なパターンマッチングにより独自言語のような形式で記述することも可能。
let json = json!({
"name": "Hoge",
"age": 23
});
・トレイトが便利
C++ではテンプレートの特殊化でトレイトを実装していましたが、
Rustでは公式にサポートされているため、簡潔に実装することが出来ます。
template<typename T>
struct objectify_trait {};
template<>
struct objectify_trait<int> {
static object_t perform(int val) { ... };
};
template<>
struct objectify_trait<float> {
static object_t perform(float val) { ... };
};
...
template<typename T>
object_t objectify(T val) {
return objectify_trait<T>::perform(val);
}
impl From<i32> for Object {
fn from(val: i32) -> Object { ... }
}
impl From<f32> for Object {
fn from(val: f32) -> Object { ... }
}
...
fn objectify<T: Into<Object>>(obj: T) -> Object {
obj.into()
}
・拡張性がすごい
トレイトによる既存の型の拡張が可能
また、ほとんどの演算子はトレイトで実装されているため、特定の型に合わせたオーバーロードも可能
struct Foo;
struct Hoge;
#[derive(Debug)]
struct FooHoge;
#[derive(Debug)]
struct HogeFoo;
impl ops::Add<Hoge> for Foo {
type Output = FooHoge;
fn add(self, _rhs: Hoge) -> FooHoge {
FooHoge
}
}
impl ops::Add<Foo> for Hoge {
type Output = HogeFoo;
fn add(self, _rhs: Foo) -> HogeFoo {
HogeFoo
}
}
fn main() {
println!("Foo + Hoge = {:?}", Foo + Hoge); // Foo + Hoge = FooHoge
println!("Hoge + Foo = {:?}", Hoge + Foo); // Hoge + Foo = HogeFoo
}
・列挙体(enum)が便利
Rustの列挙体は、なんとデータを保持できる
//データを保持できる列挙体
enum DataHolder {
Integer(i32),
Float(f64),
Text(String),
Point { x: f64, y: f64 },
}
fn print_data(data: DataHolder) {
match data {
DataHolder::Integer(n) => println!("整数: {}", n),
DataHolder::Float(f) => println!("浮動小数点数: {}", f),
DataHolder::Text(s) => println!("文字列: {}", s),
DataHolder::Point { x, y } => println!("ポイント: ({}, {})", x, y),
}
}
これをC++で実装しようと思ったら...想像したくもありません
これがデフォルトで使えるのは素晴らしい!
・未定義動作が起こらない
ダングリングポインタやメモリの多重解放、nullポインタの参照解決などは全てコンパイル時に検出される。
よって、プログラマがコンパイラのチェックをパスしたなら、未定義動作は起こりえない
・コンパイラが優しい
余りにもエラーメッセージが読みやすすぎる。数百行のエラーを吐くどこかの言語とは大違い
・モダンな文法
読みやすく、書いていて冗長さが感じられないストレスフリーな文法。
・外部のライブラリの導入が楽
このミームの通りです
引用元: https://www.reddit.com/r/ProgrammerHumor/comments/1hnfuvk/whyidliketoavoidusingcpp/?rdt=41480
・他言語との連携も容易
FFI(Foreign Function Interface)により、C言語などと簡単に連携することが出来る
・低レベルな操作もできちゃう
システムプログラミング言語としての威厳も見せてくれます
・コンパイル時評価
const fn add(a: i32, b: i32) -> i32 {
a + b
}
// コンパイル時に評価される定数を定義
const SUM: i32 = add(2, 3);
fn main() {
// コンパイル時に計算された定数SUMを利用
println!("2 + 3 = {}", SUM);
}
しかし、C++のconstexprのような柔軟性はなく、ヒープ割り当てや一部の副作用のある操作は制限されているようです
難しいと思ったところ
・所有権/借用
今まで意識してこなかった概念に加え、厳しいBorrow Checkerがビシバシ注意してくるので心が折れそうになりました。
シンプルなコードを書く分には困らなそうですが、プロジェクトが大きくなってくると
どんどん複雑になってしまいそう。完全に理解するにはまだ時間がかかりそうです
・ライフタイム
ライフタイム注釈に苦しめられました。意味的に間違っていてもメモリ安全であればコンパイルが通ってしまう為、エラー解決に悩まされました。
・関数のオーバーロードがない
これは驚きました。しかしトレイトでそれらしいものを実現できるようです
fn do_it<T: DoIt>(t: &T) -> T {
t.do_it()
}
・可変引数テンプレートがない
マクロで代用するしかないようですが、その為に動的なディスパッチが増えてしまうのは少し残念でした
・コンパイル時分岐/テンプレートメタプログラミングが出来ない
結構つらかったです。というのも、ずっとC++のSFINAEやコンパイル時分岐に頼ってきたので
いざ使えないとなると、どのように実装すればいいのかが分からなくなりました。
マスコットキャラクター
話が変わりますが、Rustにはマスコットキャラクターがいるようで
Ferris、というらしいです。つぶらな瞳が何とも愛らしい
そういえば、C++のマスコットは調べたことがありませんでした。
名はキース。肥満した病気のネズミで、後ろ足が吹き飛んでなくなっているようです。
どうやら公式のマスコットキャラクターではなく、UNCYCLOPEDIAが元ネタのようです。(安堵)
しかしC++の特徴を的確に表しているキャラクターだと思いました。もうこれでもいいのでは?
Rust、C++バージョンのライブラリを比較してみる
本にあったサンプルコードを書き写すだけでは学習にならないと思い、JNI(Java Native Interface)を使用したJavaのクラスを定義して使えるようにするクレートを作りました。一度C++でも実装したので、ちょうど比較にも使えます。
一応、C++バージョンのリンクを張っておきます
・クラスの定義
例えば、このようなJavaのクラスがあった場合
public class TestClass {
static final int STATIC_ID = 123;
int id;
TestClass(int id, float value) {
this.id = id;
System.out.println(value);
}
void printId() {
System.out.println("id: " + id);
}
static void printStaticId() {
System.out.println("static-id: " + STATIC_ID);
}
}
それぞれのクラス定義は以下のようになります
JBRIDGE_DEFINE_CLASS(your::package, YourClass, {
JBRIDGE_DEFINE_STATIC_FIELD(int, STATIC_ID)
JBRIDGE_DEFINE_FIELD(int, id)
JBRIDGE_DEFINE_METHOD(void, printId)
JBRIDGE_DEFINE_METHOD(void, printStaticId)
})
jbridge::define_class!{
your {
package {
class YourClass {
static STATIC_ID: jint;
let id: jint;
fn printId() -> ();
static fn printStaticId() -> ();
}
}
}
}
Rustはマクロが強力なため、上記のような書き方をすることが出来ました。
そのおかげで、可読性が上がり、直感的にも分かりやすくなったように感じます。
・関数 / フィールドシグネチャの生成
JNIを使用したJavaの関数の呼び出しにはシグネチャを使用する必要があります
public void function(int val) { ... } => "function", "(I)V"
ここではC++に軍配が上がりました。C++は可変引数テンプレートに対応しており、工夫すればコンパイル時に文字列を結合することが可能なので、テンプレートの特殊化を合わせコンパイル時にシグネチャを生成する事が出来ました
template<typename ReturnType, typename ...Types>
constexpr auto build_function_signature() {
return str::add_all(build_param_signature<Types...>(), build_return_signature<ReturnType>());
}
ですが、Rustはコンパイル時に文字列を結合する方法がないので、動的にシグネチャを生成することに。
MethodIDはOnceCellで管理しているため、取得処理は一度で済むようにしているのですが、やはりオーバーヘッドが気になりました
let mut signature = std::string::String::new();
signature.push_str("(");
signature.push_str(fqcnify::<$first>(<$first>::signature()).as_str());
$(
signature.push_str(fqcnify::<$at>(<$at>::signature()).as_str());
)*
signature.push_str(")");
signature.push_str(fqcnify::<$rty>(<$rty>::signature()).as_str());
・メソッドの呼び出し / フィールドの取得
int main() {
auto test_class = your::package::TestClass::new_(1, 4.5f);
test_class.printId();
your::package::TestClass::printStaticId();
std::cout << test_class.id()() << std::endl; // 1
std::cout << your::package::TestClass::STATIC_ID()() << std::endl; // 123
test_class.id() = 12;
std::cout << test_class.id()() << std::endl; // 12
}
fn main() {
let test_class = new_object!(your::package::TestClass; 1, 4.5);
test_class.printId();
your::package::TestClass::printStaticId();
println!("{}", test_class.id().get()); // 1
println!("{}", your::package::TestClass::STATIC_ID().get()) // 123
test_class.id().set(12);
println!("{}", test_class.id().get()); // 12
}
見た目にあまり差はありませんが、Rustには可変長引数がないので、コンストラクタ呼び出しはnew_objectというマクロを使用して行います。
また、代入演算子のオーバーロードも出来ないので、フィールドはgetとメソッドとsetメソッドを持つという結果になりました。
まとめ
最近は活躍の場をどんどん広げており、将来性も含め、学ぶ価値は十分にある言語と思いました。
個人的にはRustは学習コストが高く設計パターンも大きく変わるため、これからのプロジェクトでRustを使用するか、迷うところもありました。
しかし、いくつかの制約が課されるとはいえ、言語そのものとしては拡張性もあり、柔軟にコードを書くことができるかつ、
コンパイル時にはメモリ安全など、ほとんどの安全性を保障してくれるRustは、プログラマに更なる挑戦の機会を与えてくれる素晴らしい言語だと思いました。
コミュニティも活発なので、これからどのような進化を遂げるのかが楽しみです。
リンク等
作ったクレート(初めて作ったものなので、結構なクソコードです。ご容赦ください)
jbridge-rust:https://github.com/Enuwbt/jbridge-rust
JBridge(C++):https://github.com/Enuwbt/JBridge
UNCYCLOPEDIA( C = C + 1 ): https://en.uncyclopedia.co/wiki/C%2B%2B
「ベアメタル」環境でもRustを採用 Googleが「Android 14」での取り組みを解説:https://forest.watch.impress.co.jp/docs/news/1538800.html