Edited at

C++erのためのRust入門

新しい言語を勉強したかったので、Rustを始めてみる。


Rustを選んだ理由

候補言語としては、D, Go, Rustがあった。

そのうちでRustを選んだのは次のような理由からだった。


  • 演算子オーバーロードが使いたかった

    Goには演算子オーバーロードがない


  • ジェネリクス(テンプレート?)が欲しかった

    Goにはジェネリクスがないらしい


  • ガーベッジコレクタ(GC)が気に入らない

    GoDもGCをオフにすることはできるらしいが、たぶん普通のことではないだろう


念の為に言っておくと、GoDには何の非もない。

私が大きな変化についていけないだけのことである。


Rustのリリースの種類



  • Nightly masterブランチの日ごとの最終成功ビルド、新機能が追加される。


  • Beta 6週ごとにNightlyから昇格、Stableに向けてバグフィックスが行われる。


  • Stable 6週経ったBetaが昇格。


macOSにおけるRustのインストール方法


  1. Homebrewを使う (stableのみ)

    brew install rustで入れられる。

  2. rustupを使う

    curl https://sh.rustup.rs -sSf | shで入れられる。

    (URLが変わっている可能性もあるのでwww.rustup.rsを確認すること)


Rustの書き方


Rustの資料

以下全て英語。日本語訳も探せばあるかも。



  • The Rust Programming Language(TRPL)

    Rustの教科書。これを読めばまともなコードが書ける。


  • Rust By Example(RBE)

    説明よりコードを読むほうが好きな人に。TRPLのお供にもどうぞ。


  • Rust Cookbook

    具体的な処理の書き方を学びたい方へ。あんまり読んでない。


  • The Rustnomicon

    Rustを安全性の軛から解き放つunsafeのこと、またメモリレイアウト、FFIの書き方などが載っているはず。読んでない。


  • The Rust Reference

    Rustの仕様書を目指して書かれている文書。言語自体が発展途上なのでどうしても内容は古くなりがちだが役に立つ。ちょっと読んだ。

この記事は基本的にTRPLに添ってすすめる。

ただし、まとめたほうがいいと思ったものは適当に再構成する。


コメント

// 行コメント

/*
ブロックコメント
/*
ネストも可
*/

*/

/// ドキュメントコメント
/// 内部でMarkdownが使えるらしい

//! ドキュメントコメントその2
//! こちらは個々の関数等よりは、モジュール全体の説明などに使うらしい


変数の型


  • ブール型

  • 整数型 (u8,u16,u32,u64,i8,i32,i64)

  • 浮動小数点数型 (f32,f64)

  • 実装依存の整数型 (usize,isize)

    (大きさはそのプラットフォームのポインタ型と同じ)

  • 32ビット文字型

  • 8ビット文字列型

  • タプル型

  • 配列型とスライス型

  • 構造体型

  • enum型

  • 関数ポインタ型

カッコ内はリテラルに付けられる接尾辞。


変数宣言

Rustでは、変数はデフォルトで不変(定数)で、変更できるようにするにはmutをつける。

現代の言語らしく型推論もあるのでよい。

let a = 1;     // 定数

let mut b = 3; // 定数でない
let c: i32 = 0; // 型指定

変数の宣言と初期化は分けることができる。

分けた場合でも型推論ははたらく。

let i; // 宣言

i = 1; // 初期化

let mut j; // 宣言(変更可)
j = 3; // 初期化
j = 7; // 変更

複数の変数を1行で初期化するには次のようにする。

let (t,u,v) = ("alpha","beta","charlie"); // t,u,vは定数

let (mut w, mut x) = ("delta","echo"); // w,xは定数ではない
let (y,z): (&str,&str) = ("foxtrot","golf")// 型指定 (thanks to @JunSuzukiJapan)

C++にはない機能として、Rustでは変数の宣言を上書きできる。

上書きするには現在の型と異なる型で宣言しなおす。

let a: i64 = 1;

println!("{}",a); // => "1"

let a: f64 = 3.14;
println!("{}",a); // => "3.14"

TRPLの"Guessing game"に使用例がある。

特殊な変数として、割り当てを無視するための_がある。

let _ = 1; // 割り当てを捨てる

let _ = test(); // 関数の戻り値を捨てる

これは主に後述のdestructuring let、マッチやパターンと一緒に使う。

また、戻り値をかならず使うよう指定されている(must_use)関数の戻り値を捨てるために使われるらしい。


変数の属性(conststatic)

Rustにもconststaticが存在する。

constはC++11以降のconstexprのようなもの。

コンパイル時定数として扱われ、インライン化されるためメモリ上のアドレスは不定。

staticは概ねC++のものと一緒。

起動時に1度だけ初期化され、ずっと同じアドレスにいる。

これら2つは宣言時に型を指定する必要がある。

また、これらはグローバルスコープに置くことができる。

ただし、これらは宣言の上書きや宣言と初期化の分離ができない。

命名に関して、const,staticな変数には大文字の変数名を使用するという暗黙の了解があるようだ。

/* グローバルスコープ */

const C: i64 = 1; // 定数
static S1: i64 = 2; // 定数
static mut S2: i64 = 3; // 変更可


型へのこだわり

Rustは型にかなり厳しい。

暗黙変換は基本的に存在しない。

let i: i64 = 3.5;     // エラー:宣言と初期化の型が合わない

println!("{}",3/4.); // エラー:割り算のオペランドの型が一致しない

let j = 1;
let k: f64 = j; // エラー:宣言と初期化の型が合わない

もちろんキャストはある("one of the most dangerous features of Rust!")。

ただし、asstatic_cast相当のキャストにしか使えず、Cライクな「何でもキャスト」をするには別の方法が必要。

let l = 1;

let m: f64 = j as f64; // j(整数)をf64にキャスト


関数

戻り値の型を指定しない場合はユニット型(空のタプル())が戻り値になる。

それ以外のものを返したいときは型指定が必要。戻り値の型推論はない。

また、引数には型指定が必要。

i32同士を足し算する関数を書いてみる。

fn add(x: i32, y: i32) -> i32 {

x + y
}

セミコロン無しの表現がreturnの役目を果たす。

もちろんC++スタイルでreturnを書くこともできる。

関数内関数を書くこともできる。

また、C++のラムダ式にあたるクロージャもある。


関数ポインタ

先のfn add(x: i32, y: i32) -> i32の関数ポインタは

let x: fn(i32,i32) -> i32 = add;

と書ける。


配列

境界チェックがあり、領域外を参照すると異常終了。

アクセスの仕方はC++と同じで、添字は0オリジン。

アクセス時の添字はusize型でなければならない。

let a = [1,2,3,4,5]; // 5要素の配列

let b = [3;5]; // 5要素の配列、全要素を3で初期化
let c: [i32;10]; // 10要素の配列、要素の型はi32

配列は後述のスライスに暗黙変換できるので、配列自体は大した機能を持っていない。


スライス

配列の一部分に対する参照。

C++のvalarrayにあった気がする。

let a = [1,2,3,4,5]; // 配列

let whole = &a[..]; // 配列全体への参照
let part = &a[1..4]; // 第1~3要素への参照

スライスaのメソッドのうち、便利そうなものを挙げておく。

メソッド名
機能

a.contains(x)

axが含まれているかを返す

a.iter()

aのイテレータ(C++のconst_iteratorに相当)を返す。

a.iter_mut()

aの変更可能なイテレータ(C++のiteratorに相当)を返す。

a.len()

aの要素数を返す

a.reverse()

aの要素の順番を逆転させる

a.swap(x,y)

ax番目とy番目の要素を交換する

a.sort()

aをソート。(安定、$O(n\log n)$)


タプル

C++と違い、Rustでは組み込み型。

要素数とすべての要素の型が同じなら代入もできる。

let a = (1,2); // (i32,i32)のタプル

let b = (4.0,) // (f64)のタプル

要素が1個のタプルを作るときは、最後の要素の後ろにカンマが必要。

タプルの中身を取り出すには2つの方法がある。


destructuring let

let文を使って新しい変数にタプルの中身を割り当てることができる。

当然左辺と右辺で要素数が違ったりするとエラー。

let t1 = (1,2);

let t2 = (3,4);
let (x,y) = t1; // x=1, y=2となる
let (_,z) = t2; // z=4となる (_で要素を無視できる)


添え字でアクセス

添え字で要素を指定できる。添え字は0スタート。

let t = (10,20,30);

let a = t.0 // a=10
let b = t.1 // b=20
let c = t.3 // コンパイルエラー

添え字に変数は使用できない。out-of-boundsはコンパイル時にエラーとなる。


if

条件式を囲うカッコがいらない。

(TRPLから引用)

let x = 5;

if x == 5 {
println!("x is five!");
}

C++のifは文(statement)だが、Rustのifは式(expression)である。(thanks to @moriturus)

式は値を返すので、三項演算子的な使い方もできる。

(TRPLから引用)

let x = 5;

let y = if x == 5 { 10 } else { 15 }; // y = 10 で初期化


ループ

Rustには無限ループ専用の構文がある。最適化のしやすさを狙ってのことらしい。

loop{

println!("infinity!"); // 無限の infinity!
}

while文もある。

let mut i = 5;

while i < 5 {
println!("loop!");
i -= 1;
}

for文は、range-based forのような現代的な感じ。

let a = [0,1,2,3,4];

for v in a.iter() { // 配列はイテレータに変換して使う
println!("{}",v); // 0~4を順番に出力
}

数字を使いたいならこのように。

for i in 0..5 {

println!("{}",i); // 0~4を順番に出力
}

break,continueはC++の用法に加え、ラベルと併用して多重ループを上手に扱える。

(TRPLから引用)

'outer: for x in 0..10 {

'inner: for y in 0..10 {
if x % 2 == 0 { continue 'outer; } // continues the loop over x
if y % 2 == 0 { continue 'inner; } // continues the loop over y
println!("x: {}, y: {}", x, y);
}
}

(RBEから引用)

'outer: loop {

println!("Entered the outer loop");

'inner: loop {
println!("Entered the inner loop");

// This would break only the inner loop
//break;

// This breaks the outer loop
break 'outer;
}

println!("This point will never be reached");
}

println!("Exited the outer loop");


Vec

C++で言うvector

組み込み型でも何でもないが、便利だからかTRPLでは他の言語機能と並んで紹介されている。

配列と同様に境界チェックあり。

let v1: Vec<u64> = vec![]; // 空のVec生成

let v2 = vec![0;10]; // 10個の 0 を要素として生成
let v3 = vec![1,2,3,4,5]; // 要素リストから生成

for v in v3 { //for文で全要素を巡回
println!("{}",v); // 1~5を順に表示
}


所有権

よくわからないので、TRPLを参考にする。

let v = vec![1, 2, 3];

let v2 = v;
println!("v[0] is: {}", v[0]); // エラー: v はムーブ済み

Rustでは、基本的に変数の移動は(コピーでなく)ムーブとして扱われるらしい。

これは、「一つのリソースの所有権はただ一つの変数に束縛される」という原則によるものとのこと。

ただし、同じことをu64型で行っても、暗黙的にコピーされるので問題は起きない。

let x: u64 = 32;

let x2 = x;
println!("x is: {}", x); // 問題なし

暗黙のコピーが起こるようにするには、CopyというTraitが必要らしい。

プリミティブ型は基本的に1Copyを持っている。

Traitについては後述。


(不変な)参照と借用

上で述べたように、Rustではムーブが基本。

fn print_all(v: Vec<u64>){

for val in v {
println!("{}",val);
}
}

fn main(){
let v = vec![1,2,3,4,5];
print_all(v); // 問題なし
print_all(v); // エラー: v はムーブ済み
}

引数として渡すときも、ムーブで渡されてしまう。

let v = vec![1,2,3,4,5];

for val in v { // 問題なし
println!("{}",val);
}

for val in v { // エラー:vはムーブ済み
println!("{}",val);
}

for文に渡してもムーブされる。

無論、このシチュエーションを切り抜けるための策はある。

Rustでは(不変な)参照はいくつ作っても良いことになっているので、それを使う。

(厳密には、&TCopyを持つので引数として渡されるときにコピーされている、つまり「参照をコピーして渡す」ということらしい)

fn print_all(v: &Vec<u64>){

for val in v {
println!("{}",val);
}
}

fn main(){
let v = vec![1,2,3,4,5];
print_all(&v); // 問題なし
print_all(&v); // 問題なし
}

let v = vec![1,2,3,4,5];

for val in &v { // 問題なし
println!("{}",val);
}

for val in &v { // 問題なし
println!("{}",val);
}

Rustでは、このように参照を通じて所有権を借用(borrow)することができる。


変更可能な参照

不変な参照だけでは世の中を渡り歩けない。

そこで、変更可能な参照&mutを使う。

変更可能な参照を通じて変数にアクセスするには、*が必要。

(TRPLから引用)

let mut x = 5;

{
let y = &mut x;
*y += 1;
}
println!("{}", x);

変更可能な参照は一時的に所有権を移動するのと同義である。

したがって、一度に一つしか存在できない。

(ムーブはできるがコピーはできない)

また参照が存在する間は元の変数は使用できない。

上の例では、参照のスコープを抜けてからprintlnを使用している。

スコープを表す波括弧を取り除くと、エラーが起こる。


生存期間(lifetime)

(日本語名は拙訳)

よくわからないので、TRPLを参考にする。

次のようなコードを考える。

let r;

{
let i = 1;
r = &i;
}
println!("{}", r);

rに束縛された参照がprintlnで使われる時には、すでに元の変数のスコープを抜けているので何が起こるかわからない。

(C++で同じようなコードを書くと未定義?)

Rustではこのコードはコンパイルを通らない。

これだけなら、「コンパイル時にスコープチェックができるようになった」だけである。

ここからが、TRPLをして「Rustの最も独特かつ尊重すべき機能であり、Rustを利用する開発者が熟知すべきもの」と言わしめるところである。

次のコードを見てみよう。

fn skip_prefix(line: &str, prefix: &str) -> &str {

// ...
}

let line = "lang:en=Hello World!";
let lang = "en";

let v;
{
let p = format!("lang:{}=", lang);
v = skip_prefix(line, p.as_str());
}
println!("{}", v);

このコードは次のようなエラーでコンパイルを通らない。

error[E0106]: missing lifetime specifier

--> test.rs:1:45
|
1 | fn skip_prefix(line: &str, prefix: &str) -> &str {
| ^ expected lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `line` or `prefix`

参照を返す際には"lifetime parameter"なるものが必要らしい。

skip_prefixを直すと次のようになる。

fn skip_prefix<'a, 'b>(line: &'a str, prefix: &'b str) -> &'a str {

// ...
}

'a,'bが"lifetime parameter"(生存期間パラメータ)である。

'alineの、'bprefixの生存期間を表す。

返値の&'a strは、「生存期間をlineと同じにする」という設定になったのである。

linemain関数内では生きつづけるので、返値もおなじ生存期間になったことでprintln!で問題なく使用できる。

特殊な生存期間パラメータとして、'staticがある。

名前の通り、プログラム開始から終了まで生き続ける。

ちなみに、すべての参照引数に生存期間パラメータが必要なわけではないので、上の関数は次のようにも書ける。

fn skip_prefix<'a>(line: &'a str, prefix: &str) -> &'a str {

// ...
}

('bは関数内で使用されていないので省略できる)


構造体

C++とだいたい一緒。

メンバ宣言は、<メンバ名>:<型>の形。

struct Foo{

m1: u64,
m2: f32,
}

C++と違い、conststaticメンバは作れない。

(constメンバについては標準ライブラリに抜け穴が用意されているようだが)

また、メンバレベルでのアクセス制御をするには、モジュールを切り分ける必要がある。

参照をメンバとして取ることもできるが、その際には生存期間の指定が必要になる。

(「構造体が参照よりも長生きしないようにするため」らしい)

struct Foo<'a>{

m: &'a i32,
}


構造体の初期化

struct Foo{

m1: i32,
m2: i32,
m3: i32,
}

を初期化するには、次のようにする。

let f = Foo{m1: 1, m2: 3, m3: 5};

構文としては、<構造体名> { <メンバ名> : <メンバの値>, ... }となる。

このとき、メンバの順番はどうでもよいが、必ずすべてのメンバを初期化しなければならない。

また、既存の構造体インスタンスの値を一部だけ変更してコピーすることもできる。

let f1 = Foo{m1: 1, m2: 3, m3: 5};

let f2 = Foo{m1: 0, m3: 7, .. f1}; // m1: 0, m2: 3, m3: 7


タプル構造体

Rustではタプルに名前をつけて新たな型を作ることができる。

struct A(u64);

struct B(u64);

let a = A(1);
let b = B(2);
// b = a は不可能(AとBは違う型)

要素にアクセスするときは、普通のタプル同様に添え字アクセスができる。

また、destructuring letもすこし書き方が違うが可能である。

struct Test(u64,u64,u64);

let t = Test(1,2,3);
let Test(_,i,j) = t; // i=2, j=3


空の構造体 (Unit-like struct)

Rustでも要素のない構造体を作ることができる。

struct Empty1{}  //要素のない通常の構造体

struct Empty2; //要素のない構造体(特別な記法)
struct Empty3(); //要素のないタプル構造体

/* 生成方法 */
let e1 = Empty1{};
let e2 = Empty2;
let e3 = Empty3();


列挙型(enum)

C++のenumとは違って、整数型以外の型も扱える。

もっと言えば、各要素が異なる型であっても良い。

いわゆるタグ付き共用体のようなもの。

enum Test{

A, // 空の構造体
B(i32), // (i32)のタプル
C{x: f64} // 構造体
}

let x = Test::A;
let y = Test::B(16);
let z = Test::C{x: 3.14};


構造体などのメソッドの実装

構造体Fooを次のように定義する。

struct Foo{

m: i32
}

構造体Fooにメソッドを実装するには、implブロックを使う。

C++のconststaticにあたるメソッドも実装できる。

impl Foo{

fn answer() -> i32 { // staticメソッド
42
}

fn get_m(&self) -> i32 { // constメソッド
self.m
}

fn set_m(&mut self, m_new: i32){ // メンバを変更するメソッド
self.m = m_new;
}
}

C++との違いとして、staticでないメソッドは引数でself(あるいはその参照)を取る必要がある。

selfも引数でしか無いので、メンバを利用する際にはself.でプレフィックスする必要があるし、selfを参照で取らないとムーブされてしまいややこしいことになる。

(「このメソッドを使うならこの構造体の役目は終わり」ということであえてムーブしてしまうこともあるようだが)

生存期間付きの構造体のメソッドを定義するときは、implブロックにも生存期間を書かないといけない。

impl<'a> Foo<'a>{

// ...
}

なお、タプル構造体や列挙型についても同様にメソッドを定義できる。


マッチ


マッチの基本

matchはC++のswitchのようなもの。

(TRPLから引用)

let x = 1;

match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}

また、enumにマッチすることもできる。

(ついでに便利な書き方も紹介)

enum Test {

A,
B,
C,
D,
};

let x = Test::A;

match x {
Test::A => println!("A"),
Test::B => println!("B"),
Test::C | Test::D => println!("C or D"); // 複数の状況にマッチできる
_ => {}, // 何もしない
}

_はC++で言うdefaultの役割を果たしている。

(根底にはもう少しややこしい仕組みがある、後述)

Rustのmatchは、対象となる変数が(型として)取りうる値全てについて処理を書かないといけない。

特定の値についてのみ処理を書きたいなら、残りは_で処理すれば良い。


マッチは「式」

ifが式であったように、matchも式なので値を返すことができる。

ifの節で書いた式と同じものをmatchで書いてみる。

let x = 5;

let y = match x {
5 => 10,
_ => 15
} // y = 10 で初期化

この例だとif式で書いた方がましだが、後々enumとの組み合わせ等で本領が発揮される。


まだ記事に書いてないC++との違い


  • 関数オーバーロードがない(演算子オーバーロードはできる)

  • デフォルト引数がない

  • 構造体同士の継承はできない(Trait同士の継承はできる)


  • constexpr関数が弱い


まだまだあるけど

続きは随時追記していきます


最後に

Rustユーザーの方のツッコミお待ちしております。





  1. 配列、スライスはCopyを持たず、タプルは全要素がCopyを持つ型であるときのみタプル自身がCopyを持つ