Rustのバイト列リテラルの型
Rustのリテラルの型検査は check_lit
にある。これによると、文字列リテラルの型は &'static str
である一方、バイト列リテラルの型は &'static [u8]
ではなく、 &'static [u8; N]
である。
そのため、以下のようなコードがコンパイルできる。
fn main() { let x : &[u8; 5] = b"Hello"; let y : [u8; 5] = *b"Hello"; }
なお、 b"Hello"
は、 &[72, 101, 108, 108, 111]
と等価ではない。数値リテラルが u8
とは限らないという点のほかに、生存期間が異なる。
fn main() { let x : &'static [u8] = b"Hello"; let y : &'static [u8] = &[72, 101, 108, 108, 111]; }
rustc 1.20.0-nightly (3bfc18a96 2017-06-29) error[E0597]: borrowed value does not live long enough --> <anon>:3:30 | 3 | let y : &'static [u8] = &[72, 101, 108, 108, 111]; | ^^^^^^^^^^^^^^^^^^^^^^^^ does not live long enough 4 | } | - temporary value only lives until here | = note: borrowed value must be valid for the static lifetime... error: aborting due to previous error(s)
生存期間を考慮すると、 b"Hello"
はおよそ以下のように脱糖されると考えられる。
fn main() { let x : &'static [u8] = b"Hello"; let y : &'static [u8] = { static S : [u8; 5] = [72, 101, 108, 108, 111]; &S }; }
Rust unsize期待型とキャスト期待型
Rustの期待型は以下のようなデータ構造になっている。
#[derive(Copy, Clone, Debug)] pub enum Expectation<'tcx> { NoExpectation, ExpectHasType(Ty<'tcx>), ExpectCastableToType(Ty<'tcx>), ExpectRvalueLikeUnsized(Ty<'tcx>), }
このように4つのコンストラクタを持つ Expectation
だが、最も大きな働きをしているのは NoExpectation
と ExpectHasType
であり、ほぼ Option<Ty>
と思って問題ない。
この記事ではまず、残りの2つの動作について把握する。これは ExpectHasType
よりも弱い期待をするもので、ごく限定された場面でのみ生成される。
unsize期待型
ExpectRvalueLikeUnsized(U)
は、その式の型が T: Unsize<U>
であるような T
であることを期待するものである。
この期待型は rvalue_hint
によってのみ生成される。この関数は、
[T]
,str
,Trait
型に対してはExpectRvalueLikeUnsized
- それ以外の型に対しては
ExpectHasType
を期待する。この処理は以下の部分で行われる。
- 関数呼び出し引数の推論された期待型を推論するとき。
- 関数呼び出し引数を型強制するとき。 (
[T]
,str
,Trait
に対しては型強制は実行されない) box x
式の期待型がBox<T>
だったとき。&x
/&mut x
式のx
が左辺値で、期待型が&'a T
/&'a mut T
だったとき
これらに共通するのは、これらが全て Sized
な式を期待しているという点である。 (&x
で x
が左辺値なら x
は !Sized
かもしれない)
ExpectRvalueLikeUnsized
は to_option
によって取り出される。これを調べると、 ExpectRvalueLikeUnsized
は以下の用途にしか使われていないことがわかる。
- 配列リテラルの要素に対して型強制を行う。 (
[a, b, c]: ExpectRvalueLikeUnsized([T])
ならa
はT
に型強制される)- もちろん、
ExpectRvalueLikeUnsized
以外の期待型についてもこれは行われる。
- もちろん、
例えば以下の例では、配列の要素に対して型強制が行われている。
fn main() { let x : Box<[*const i32]> = Box::new([&1]); }
キャスト期待型
ExpectCastableToType(T)
は、その式の型が x as T
で変換可能であることを期待するものである。
これは as
式の型検査に対してのみ生成される。逆に、これが利用されるのは以下の場面のみである。
前者については、以下の例を見るとわかる。
fn main() { println!("{}", 2000000000000 as i64); // 2000000000000 println!("{}", (1000000000000 + 1000000000000) as i64); // -1454759936 }
これは以前の記事で紹介した仕組みだけでは説明できない。以前の記事で紹介した仕組みにより、 1000000000000 + 1000000000000
はこの左辺・右辺と同じ型をもつことが仮定される。そのため上の 2000000000000
が i64
になるなら原則として 1000000000000
も i64
になるはずだが、そうなっていない。
これは、ここで 2000000000000
が i64
になる仕組みが、型推論ではなく、期待型により実現されているからである。ここで説明したように x as i64
は内側の x
に対して ExpectCastableToType(i64)
を生成する。これを受けた整数リテラルは、サフィックスを持たないリテラルの型を i64
とおく。そして ExpectCastableToType
は演算子の内側には伝搬しないため、 1000000000000
は i64
とはならず、したがってデフォルトの i32
になってしまう。
後者には以下のような奇妙な例がある。
fn main() { let x = [&1] as [*const i32; 1]; }
これはコードの形に反して、 [&i32; 1]
から [*const i32; 1]
へのキャストではなく、 &i32
から *const i32
への型強制が行われている。
また、 ExpectCastableToType
は if
式の内側に伝搬しない という特徴がある。理由はその部分のコメントにあるように、then節側で厳しすぎる推論をしてしまうことを防止するためである。
Rust 関数呼び出しの期待型の伝搬
以前の記事で説明したように、Rustでは型強制や整数リテラルなどの特殊な型推論をシームレスに行うために期待型という概念を導入している。
この期待型はトップダウンに伝搬されるが、先ほどの記事で挙げた例
fn main() { let x : Box<[_]> = Box::new([1, 2, 3]); // ^^^^^^^^^^^^^^^^^^^ coercion site let y : Box<Box<[_]>> = Box::new(Box::new([1, 2, 3])); // ^^^^^^^^^^^^^^^^^^^ coercion site }
では Box::new
関数の外側から内側に向けて期待型の伝搬が行われている。
これは Box::new
に限ったことではなく、一般にどのような関数でもこのような伝搬が行われる。この計算をしているのが expected_inputs_for_expected_outputs
メソッドである。
これは大まかに言うと次のようなことをしている。
- 関数の引数型と戻り値型を生成する。ジェネリクスにより未確定の部分には型推論変数を入れる。例えば、
Box::new
の場合、引数型は?T
で戻り値型はBox<?T>
である。 - 型推論環境のスナップショットを生成する。
- 関数の戻り値型と、関数呼び出しの期待型で、単一化を行う。例えば期待型が
Box<Box<[?X]>>
なら、単一化により?T := Box<[?X]>
が生成される。 - 引数型の型推論変数を展開する。今回は
?T := Box<[?X]>
が判明しているため、引数型は?T
からBox<[?X]>
にリファインされる。 - 展開結果を、引数の期待型として覚えておく。
- 型推論環境のスナップショットをロールバックする。
期待型はあくまでヒントであるため、期待型とは異なる型が推論されることはある。例えば、上の例ではまず Box::new([1, 2, 3])
に Box<[?X]>
が期待されているという情報から [1, 2, 3]
に [?X]
が期待されているという情報が生成される。このような型強制はできないため失敗し、 Box::new([1, 2, 3])
の型は Box<[{integer}; 3]>
になる。そこで翻ってこの期待型 Box<[?X]>
と比較すると、この時点で型強制が可能であるため、強制後の型は Box<[{integer}]>
となる。
このとき、期待型の伝搬のためにまず ?T := [?X]
がユニフィケーションで生成されるが、これはロールバックにより消滅する。中の型が確定してからあらためてユニフィケーションが行われ、このときは ?T := [{integer}; 3]
となる。
このように期待型の伝搬の過程では誤った単一化子が生成されることがあるが、期待型の処理ごとにロールバックすることで競合を回避している。
Rustにおける演算子の型推論の特殊ルール
概要: 原則として x + y
は ::std::ops::Add(x, y)
の構文糖衣であるが、型推論で特別扱いされる。
演算子の脱糖
演算子の脱糖は型推論の後、HIRからMIRへの変換のタイミングで行われる。原則として x + y
は ::std::ops::Add(x, y)
の構文糖衣である。これは *x
/x[i]
以外の他の演算子についても同様である。しかし型推論では以下のような特殊扱いがある。
fn main() { use std::ops::Add; let x : i8 = 1 + 1; // OK let x : i8 = Add::add(1, 1); // Error }
下側のコードがエラーになる理由
Rustの演算子はオーバーロード可能であり、型の制約が少ない。具体的には「左辺と右辺の型から、計算結果の型が一意に決まる」ということだけが要請される。そのため型 A
と型 B
の値を足して型 C
の値ができることがありえる。例えば、以下のような足し算を定義することができる。
fn main() { use std::ops::Add; struct A; struct B; impl Add<B> for A { type Output = i8; fn add(self, other: B) -> Self::Output { 42 } } let x : i8 = 1i8 + 1i8; // OK let x : i8 = A + B; // OK }
つまり、外側の型 i8
から、内側の型を決めることは基本的にできない。演算子に限らず、戻り値型が <A as Add<B>>::Output
のような射影型になっているときは、型はボトムアップにしか伝搬しない。
つまり、 Add::add(1, 1) : i8
から 1 : i8
は推論されない。この型情報の不足から、 1: {integer}
だけが判明する。これは後でデフォルトである i32
と推論されるが、すると Add::add(1i32, 1i32) : i32
であり型が一致しない。
上側のコードがエラーにならない理由
lhs + rhs
で、 lhs
, rhs
がある特定の型の場合に、Rustは「lhs
, rhs
, lhs + rhs
の型は全て同じ」という制約を追加する。これにより自動的に内側の 1
が i8
であることが判明する。
演算子ごとの動作の違い
二項演算子は以下の4種類に分類される。それぞれ、特定の条件下で型に対して追加の仮定をおく。
- 短絡回路演算子:
||
,&&
- 常に、左辺/右辺/戻り値は
bool
である。
- 常に、左辺/右辺/戻り値は
- シフト演算子:
<<
,>>
- 両辺がともに整数型であるとき、左辺の型と戻り値の型は等しい。
- 数学演算子:
+
,-
,*
,/
,%
- 両辺がともに整数型であるか、両辺がともに浮動小数点数型であるとき、左辺の型と右辺の型と戻り値の型は等しい。
- ビット演算子:
|
,&
,^
- 両辺がともに整数型であるか、両辺がともに浮動小数点数型であるか、両辺がともに
bool
であるとき、左辺の型と右辺の型と戻り値の型は等しい。
- 両辺がともに整数型であるか、両辺がともに浮動小数点数型であるか、両辺がともに
- 比較演算子:
==
,!=
,<
,<=
,>
,>=
- 両辺がともにスカラー型であるとき、左辺の型と右辺の型は等しく、戻り値の型は
bool
に等しい。
- 両辺がともにスカラー型であるとき、左辺の型と右辺の型は等しく、戻り値の型は
ただし、スカラー型とは、 bool
/char
/整数型/浮動小数点数型/関数定義/関数ポインタ/生ポインタ のいずれかである。
「整数型である」「浮動小数点数型である」というのは、 i32
や usize
のような特定の型のほかに、 {integer}
のような推論型も含む。
単項演算子の場合
3つある単項演算子 *
/!
/-
のうち、 !
と-
も似たような動作をする。ただし以下の違いがある:
!
は、被演算子が整数または bool
のとき、 -
は、被演算子が整数または浮動小数点数のときに、組込み演算子とみなされる。このとき戻り値の型は被演算子の型と等しいと仮定される。
まとめ
Rust パターンマッチの変数束縛とコンストラクタ/定数の区別
パターンマッチを持つ言語では、変数束縛とコンストラクタ/定数が構文上曖昧である場合がある。Rustでは以下の規則に従っている。
- 以下のように、構文的にパスであるとわかる場合は、常にコンストラクタ/定数とみなす。
::
を含んでいる場合。 (::A
,self::A
,<T as Trait>::X
など){}
を後続する場合。 (A {}
など)()
を後続する場合。 (A()
など)..
/...
/..=
などの一部である場合。 (FOO_MIN .. FOO_MAX
など)!
を後続する場合。 (your_macro_expanding_to_a_pattern!()
など)
- 反駁不可文脈では常に変数束縛とみなす。反駁不可文脈かどうかは以下のように定義される。
match
,if let
,while let
のパターンは反駁可能でもよい。let
,for
, 関数の引数部のパターンは反駁不可でなければならない。
- asパターン (
ident @ pat
) の左辺は常に変数束縛とみなす。 ref ident
やmut ident
のように束縛モードが指定されていれば常に変数束縛とみなす。- 上記以外で、スコープ内のアイテム名と一致し、それらが以下のいずれかだった場合はコンストラクタ/定数とみなす。
const
定数static
静的変数- ユニット形式の
struct
(struct A;
のように宣言されるもの) enum
のバリアントでユニット形式のもの (例:use E::A; enum E { A; }
)
- 上記以外では、変数束縛とみなす。
また、上記の条件で変数束縛とみなされたが、アイテム名と一致するときはエラーとなる。
例えば以下のようになる。
struct A; fn main() { // let A = A; // Error let self::A = A; // OK match A { A => {}}; // OK // match A { ref A => {}}; // Error // match A { A @ A => {}}; // Error }
Rustで use std; が必要なときとエラーになるときがあるのは何故か
Rustでは use std;
や use rand;
のようなインポートが必要な場合と、逆に書くとエラーになる場合がある。
簡単に言うと
- トップレベルモジュールでは、書くとエラーになる(既にある名前と衝突する)。それ以外の場所では、必要な場合がある。
- これは名前をローカルで使うための仕組みと、名前の別名を公開するための仕組みが同じ
use
により実現されていることと関係している。
そもそも use std;
は何故必要なのか
例えば、あるモジュール m1.rs
内で以下のようなコードを書くとエラーになる。
fn foo() { let stderr = std::io::stderr(); }
rustc 1.18.0 (03fc9d622 2017-06-06) error[E0433]: failed to resolve. Use of undeclared type or module `std` --> m1.rs:2:18 | 2 | let stderr = std::io::stderr(); | ^^^^^^^^^^^^^^^ Use of undeclared type or module `std` error: aborting due to previous error
呼ぼうとしているのは確かに std::io::stderr
なのにこれが発見されないのは何故か。これは、この手のパスが文脈によって異なる解釈をされることに由来する。そもそもパス(::
で区切られているやつ)は次の3種類に分けられる。
::
または$crate::
で始まるもの (例:::std::default::Default
)- これらは絶対パスとして解釈される。
self
またはsuper
で始まるもの (例:self::MyTrait
) (※単独のself
は例外)- これらは相対パスとして解釈される。
- それ以外
つまり、 std::io::stderr
はここでは絶対パスではなく、相対パスのようなものとして扱われてしまっているのである。
絶対パスとして修正する場合
先ほどのコードで意図しているのは絶対パスだと考えられる。したがって次のようにすればエラーにはならない。
fn foo() { let stderr = ::std::io::stderr(); }
レキシカルスコープのパスとして修正する場合
もう一つの解釈として、レキシカルスコープのパスとしての使用を意図していた可能性も考えられる。例えば、以下のコードは正しい。
use std::io; fn foo() { let stderr = io::stderr(); }
同じ要領で、 std
をインポートすればよいという考えがありえる。これも正しい。
use std; fn foo() { let stderr = std::io::stderr(); }
トップレベルモジュールでは不要なのはなぜか
トップレベルモジュールでは通常、 extern crate
が多く行われている。例えば、 extern crate rand;
と書いておくと、 rand
のトップレベルモジュールが ::rand
にリンクされる。
ここでもし m1
モジュール内で use rand;
をすると、これがさらに ::m1::rand
にリンクされることになる。これにより m1
の直下で rand
を簡単に参照できるようになる。
同じ理屈で、トップレベルモジュールで use rand;
をすると、 ::rand
を ::rand
にリンクしようとしていることになる。これはおかしいのでエラーになってしまうという寸法である。
こういった理屈のため、 use nalgebra as na;
のような別名インポートはトップレベルモジュールでも必要である。例えば、
extern crate nalgebra; use nalgebra as na; .. mod m1 { use nalgebra as na; }
のようになる。もちろん、以下のようにそもそも extern crate
の時点で別名をつけてリンクすることもできる。(好ましい習慣かは別として)
extern crate nalgebra as na; // use 不要 mod m1 { use na; }
Rustの use
の設計について
Rustの use
は2つの目的を兼ねている。これは例えばこのNiko Matsakis氏のコメントでも確認できる。
- ある定義(関数や型など)に別名をつける(別名をつけて公開する)こと。 (再エクスポート)
- ある定義をローカルから使うためにスコープに入れること。 (インポート)
前者はモジュールグラフの切り貼り自体を目的にしているのに対して、後者は let
などと同様にレキシカルスコープへの名前の導入ができればよく、グラフの切り貼りは意図していない。しかしRustでは両者を同じように、グラフの切り貼りで実現する仕組みになっている。このことは、 use
の動作を理解するにあたって押さえておくとよいところかもしれない。
まとめ
- トップレベルモジュールでは、
use std;
を書くとエラーになる(既にある名前と衝突する)。それ以外の場所では、use std;
が必要な場合がある。 - これは名前をローカルで使うための仕組みと、名前の別名を公開するための仕組みが同じ
use
により実現されていることと関係している。
Rustでunsafeが必要な操作
Rustでは unsafe
の表明を行わない限り未定義動作が発生しない。コンパイラや言語仕様のミスにより未定義動作が発生しうる場合には優先的に修正される。(なお、整数オーバーフローのように特定条件下でエラーになるものであっても、正しくunwind/abortできるものは未定義動作とは呼ばない。)
unsafe
の表明
unsafe
の表明は、構文的には以下のいずれかである。
unsafe
ブロックunsafe impl
unsafe
を表明した場合、その部分では「安全かどうかをコンパイラが保証できない操作」が行えるため、この部分が安全であるかどうかを検査するのはプログラマの責任であるということになる。それぞれの操作には、それが安全に実行できるための事前条件(safety precondition)が決められているから、これが成り立っていることをプログラマが自分で保証する、という算段である。
unsafe
の要求
unsafe
を用いたライブラリ関数の場合、「関数が何らかの性質を満たしているときは安全」という状況が考えられる。例えば、比較関数が正当であることを前提としたソートアルゴリズムの実装というのが考えられる。
Rustでは原則としてこれは許されない。ライブラリの呼び出し側が (unsafe
だったり、可視性を破壊したりしていない範囲内で) どんな異常な引数で関数を呼んでも、その関数は安全に動作しないといけない。
どうしても呼び出し側に安全性の責任を転嫁したい場合は、関数シグネチャに unsafe
をつける。これにより、そのライブラリ関数は、安全に使うための追加の事前条件(safety precondition)を要求していることになる。その事前条件はドキュメントに説明されるべきということになる。
このように unsafe
を要求する構文には以下のものがある。
- 関数シグネチャの
unsafe
unsafe trait
なお、関数定義の unsafe
は要求と表明を兼ねている。
unsafe
ブロック/関数内でないとできない操作の一覧
unsafeが適切に呼ばれているかどうかは rustc::middle::effect
モジュール で検査されている。
以下では事前条件も書いてみるが、すべて筆者による推測である。
unsafe
関数/unsafe
メソッドの呼び出し
unsafe
のついている関数やメソッドを呼び出したときに発生する。関数を取り出して呼び出していない場合は発生しない。
事前条件: その関数/メソッドによって指定されている事前条件を守る。
生ポインタの参照外し
*const T
や *mut T
型の値 p
に対し、 *p
を行う。 &*p
のように左辺値の場合 (生ポインタを参照に昇格するのに使う) にも unsafe
が必要である。
事前条件・不変条件: おそらく以下のような条件が必要とされている。
- アラインメントが揃っている。
- 有効な場所を指している。
- 有効な値が格納されている。または、昇格した参照が使用されるまでに有効な値が格納される。
- エイリアスを持たないか、これ自身を含む全てのエイリアスが読み取り専用として扱われている。
インラインアセンブリ
asm!()
や global_asm!()
によるインラインアセンブリの埋め込みは常に unsafe
である。
事前条件・不変条件: Rustのもつ全ての不変条件を守ること。例えば、書き込み借用できるメモリ以外に書き込まない。不正な値を書き込まない。 Copy
でない値をコピーした場合に元の位置のデストラクタを呼んではいけない。など。
static mut
変数へのアクセス
static mut
で宣言された静的変数の読み取り/書き込みアクセスは unsafe
である。
事前条件・不変条件: 書き込み参照するときは、他に誰かが参照していないこと。読み取り参照するときは、他に誰かが書き込み参照していないこと。(他スレッドからのアクセスも含む)
extern { static }
変数へのアクセス
extern { static X : u32; }
のように、FFIで外部の静的変数を参照する変数への読み取り/書き込みアクセスは unsafe
である。
事前条件: 値を取り出す場合は、不正な値が入っていないよう注意する。
なお、この条件は互換性のために現在は警告扱いになっている(warning cycle)。将来はエラーとなる予定である。
union
要素へのアクセス
union
要素へのアクセス (読み取り、代入、パターンマッチによる読み取り) は unsafe
である。
事前条件・不変条件:
- 読み取りでは、そのフィールドに有効な値が入っていること。
- 書き込みでは、この
union
の何らかの不変条件を保ち、結果的にDrop
が正常に動作すること。 (Drop
を実装していなければ問題ない)
なお、最近の変更により、 Copy
な union
の要素への代入は unsafe
ではなくなった。