Rustのderiveはあまり頭がよくない
概要: Rustの derive
はあまり頭がよくない。
derive
がドジを踏む例
derive
の問題は顕在化しやすく、RustコンパイラのGitHub上でも何度も重複するissueが投げられていた。今は主に #26925 を中心に議論がまとまっているので、そちらを参照するとよいだろう。
不必要な境界を与える例
use std::rc::Rc; // 本来不必要な X: Clone を要求する #[derive(Clone)] struct A<X>(Rc<X>); struct B; fn main() { A(Rc::new(B)).clone(); // Error }
error: no method named `clone` found for type `A<B>` in the current scope --> <anon>:10:19 | 10 | A(Rc::new(B)).clone(); // Error | ^^^^^ | = note: the method `clone` exists but the following trait bounds were not satisfied: `B : std::clone::Clone` = help: items from traits can only be used if the trait is implemented and in scope; the following trait defines an item `clone`, perhaps you need to implement it: = help: candidate #1: `std::clone::Clone` error: aborting due to previous error
必要な境界が不足している例
struct A<X>(X); impl<X: Copy> Clone for A<X> { fn clone(&self) -> Self { A(self.0) } } // 本来必要な X: Copy を要求しない // Error #[derive(Clone)] struct B<X>(A<X>); fn main() {}
rustc 1.17.0 (56124baa9 2017-04-24) error[E0277]: the trait bound `X: std::marker::Copy` is not satisfied --> <anon>:10:13 | 10 | struct B<X>(A<X>); | ^^^^^ the trait `std::marker::Copy` is not implemented for `X` | = help: consider adding a `where X: std::marker::Copy` bound = note: required because of the requirements on the impl of `std::clone::Clone` for `A<X>` = note: required by `std::clone::Clone::clone` error: aborting due to previous error
展開結果を見るにはnightlyコンパイラで rustc --pretty expanded src/main.rs
とやるとよい。
#![feature(prelude_import)] #![no_std] #[prelude_import] use std::prelude::v1::*; #[macro_use] extern crate std as std; use std::rc::Rc; // 本来不必要な X: Clone を要求する struct A<X>(Rc<X>); #[automatically_derived] #[allow(unused_qualifications)] impl <X: ::std::clone::Clone> ::std::clone::Clone for A<X> { #[inline] fn clone(&self) -> A<X> { match *self { A(ref __self_0_0) => A(::std::clone::Clone::clone(&(*__self_0_0))), } } } struct B; fn main() { A(Rc::new(B)).clone(); // Error }
#![feature(prelude_import)] #![no_std] #[prelude_import] use std::prelude::v1::*; #[macro_use] extern crate std as std; struct A<X>(X); impl <X: Copy> Clone for A<X> { fn clone(&self) -> Self { A(self.0) } } // 本来必要な X: Copy を要求しない // Error struct B<X>(A<X>); #[automatically_derived] #[allow(unused_qualifications)] impl <X: ::std::clone::Clone> ::std::clone::Clone for B<X> { #[inline] fn clone(&self) -> B<X> { match *self { B(ref __self_0_0) => B(::std::clone::Clone::clone(&(*__self_0_0))), } } } fn main() { }
deriveはマクロの仲間
derive
はマクロと同様の構文拡張に分類される。Rustコンパイラは原則として「構文解析→構文拡張の展開→名前解決」の順で進む(ただし、パスマクロの解決のために名前解決が先に実行されることはある)ため、 derive
実行時は名前解決がなされておらず、型の情報もほぼない状態である。
deriveの境界生成規則
derive
は境界を決めるために、以下のようにして型を収集する。
- 全ての型引数。
- フィールドの型の一部分として登場する型のうち、型引数名で始まるパス型。ASTでの修飾パスのデータ構造の性質から、以下のいずれかの条件を満たせばマッチする。
X::Output::Item
のように、型引数名で始まるパス。<Frob>::X::Output
のようなパス (QSelfが削除されてX::Output
になってしまう) (※おそらくバグ)<Frob as X::Output>::Bar::Baz
のようなパス (QSelfが削除されてX::Output::Bar::Baz
になってしまう) (※おそらくバグ)
おそらくバグだが、 <X>::Output
のように書くとマッチしない。
例えば derive(Clone)
の場合は、このようにして集めた各型に対して、 Clone
を実装すべしという制約を課す。他の多くの derive
も同じ機構を用いている。
なぜフィールド型に直接境界を課さないのか
上記のようなヒューリスティクスを用いず、フィールド型に直接 Clone
を課せるようにするのが好ましいが、以下のような問題がある。
問題1: 可視性
現在のRustでは “private in public” 制約というものがある。詳しくは過去の記事を参照。 これにより以下のようなコードがエラーになる。
#[derive(Clone)] struct A<X>(X); pub struct B<X>(A<X>); // Error impl<X> Clone for B<X> where A<X>: Clone { fn clone(&self) -> Self { B(self.0.clone()) } } fn main() { let x = B(A(0u32)); x.clone(); }
B<X>
は公開される型だがそのメンバ型 A<X>
は非公開である。そのため X: Clone
は課すことができても A<X>: Clone
を課すことはできない。
問題2: 再帰的な制約
以下のように再帰的な型に対して derive(Clone)
をすることを考える。現在のヒューリスティックスでは正しく動作するが、以下のようにフィールド型に直接 Clone
を課すと、実体化のタイミングで再帰エラーになる。なお、実体化させない限りは問題が顕在化しない。
struct Node<T>(Option<Box<(Node<T>, T)>>); impl<T> Clone for Node<T> where Option<Box<(Node<T>, T)>>: Clone { fn clone(&self) -> Self { Node(self.0.clone()) } } fn main() { let x = Node(Some(Box::new((Node(None), 0u32)))); x.clone(); // Error }
問題3: 後方互換性
トレイトは実装を増やしても減らしても互換性がない。そのため、今から derive(Clone)
の挙動を変えると大きな破壊的変更となる可能性がある。
まとめ
Rustの derive
は構文的に実装を生成するだけなので、推論されたトレイト境界が間違っている場合がある。これを正しく動作させるにはいくつかの困難があると予想される。
Rustのスレッドローカル変数について
Rustには2種類のスレッドローカル変数がある。
#[thread_local]
属性
#[thread_local]
属性は、指定した static
アイテムをスレッドローカルにするようにLLVMに伝える。これによりC言語のスレッドローカル変数と同様に、ELFリンカ側の処理によりスレッドローカル変数が実現される。
この方法には以下のような問題点がある。
- 一部のプラットフォームでは利用できない。
- 現状では、不要な
Sync
が要求される。 - 参照を取得後にスレッドが終了するとUBになるため健全性が保証されていない。
- 以上のような理由があってか、安定入りしていない。そもそも
thread_local!
があるので廃止してもよいのではないかという意見もある
thread_local!
マクロ
thread_local!
マクロは、スレッドローカル変数に安全にアクセスするためのラッパーを生成する。例えば、
thread_local! { static X : Cell<u32> = Cell::new(0); }
と書いたら、実際には以下のようなアイテムが生成される。
static X : LocalKey<Cell<u32>> = LocalKey { ... };
thread_local!
は、もともとN:Mの並行性モデルを採用していたRustがグリーンスレッドを廃止して1:1に移行するにあたり、タスクローカル変数を提供するための local_data!
を整理する形でできたものである。現在のネイティブスレッドの観点からも、このマクロ以下の点で優れているといえる。
- 参照の有効期間が
LocalKey::with
により制限されるため、健全性の問題が解決されている。 - ELFリンカに依存したスレッドローカル領域が提供されていない場合も、OS非依存の代替手段が利用される。
オブジェクトごとのスレッドローカル変数
以上とは別の話だが、オブジェクトごとにスレッドローカルなフィールドを持つ必要がある場合は thread_local
crate というライブラリが使えるようだ。
まとめ
Rustでスレッドローカル変数を使うときは、 thread_local!
マクロを使うのが望ましい。これは LocalKey
という安全で移植性の高いラッパーを提供している。
Rustにおけるキーワードの役割一覧
概要: Rustにおけるキーワードの役割をできる限り列挙する。キーワードではないものについても一部取り上げる。
強キーワード部門
キーワード | 役割 |
---|---|
as |
修飾パス <T as Tr>::foo useとextern crateにおける別名 use foo as bar; 型の変換 x as usize |
box |
box式 box 1 boxパターン box x |
break |
break式 break 'a 1; |
const |
定数・関連定数 const X : i32 = 0; 関数型・関数宣言・関数定義 const fn f() 生ポインタ型 *const u8 |
continue |
continue式 continue 'a; |
crate |
extern crate extern crate combine; pub(restricted)構文 pub(crate) mod m; |
else |
if-else if b {} else {} if-let-else if let p = e {} else {} |
enum |
列挙型 enum E { ... } |
extern |
関数型・関数宣言・関数定義 extern "C" fn f() 外部アイテム extern "C" { ... } extern crate extern crate combine; |
false |
bool型の定数・パターン |
fn |
関数型・関数宣言・関数定義 fn f() |
for |
トレイト実装 impl Tr for T {} 高階の関数ポインタ for<'a> fn f(&'a u8) 高階のトレイトオブジェクト・トレイト境界 for<'a> Tr<&'a T> 高階のwhere節 where for<'a> &'a [u8]: Tr<'a> for式 for i in (0..10) {} |
if |
if式 if b {} else {} match腕のガード match x { Some(t) if t < 0 => { ... }, ... } |
impl |
実装 impl T {} impl Tr for T {} 匿名型 fn f() -> impl Fn() |
in |
pub(restricted)構文 pub(in path::to::module) fn f(); 配置構文 in place { expr } for式 for i in (0..10) {} |
let |
let文 let x = 0; if let/while let式 if let Some(x) = x {} |
loop |
loop式 loop {} 'a: loop {} |
match |
match式 match x { Some(y) => {}, None => {}} |
mod |
モジュール mod foo; mod foo {} |
move |
クロージャ(ムーブキャプチャー) move |x, y| z 廃止されたムーブ参照? &move T |
mut |
書き換え可能な生ポインタ型 *mut T 専有参照型 &mut T 可変static static mut X : i32 = 0; 可変束縛 let (x, mut y) = (1, 2); 専有借用 &mut x 専有参照外しパターン &mut p |
pub |
可視性 pub pub(self) pub(super) pub(crate) pub(in path) |
ref |
借用パターン ref p |
return |
return式 return return x |
self |
相対パス self::foo::bar pub(restricted)構文 pub(self) self引数 fn f(self: Box<Self>) self引数ショートカット fn f(&self) |
Self |
Self型引数 fn new() -> Self; |
static |
外部staticアイテム extern "C" { static X : u32; } staticアイテム static X : u32 = 0; |
struct |
構造体 struct S {} |
super |
相対パス super::super::X |
trait |
トレイト trait Tr {} |
true |
bool型の定数・パターン |
type |
型別名 type A<X> = Box<Box<X>>; 関連型(トレイトまたはトレイト実装) impl Tr for T { type X = (); } |
unsafe |
トレイト unsafe trait Send {} 関数型・関数宣言・関数定義 unsafe fn f() 実装 unsafe impl Send for T {} unsafe式 unsafe { ... } |
use |
インポートと再エクスポート use foo::bar; |
where |
where節(トレイト・実装・構造体・共用体・列挙型・型別名・関数宣言・関数定義) where T: Clone |
while |
while式 while b {} |
弱キーワード部門
弱キーワードは特定の文脈でキーワードに準じる扱いを受ける識別子のことである。
キーワード | 条件 | 役割 |
---|---|---|
default |
impl 内、 fn /type /const の前※実装アイテムマクロ名として使えないがバグかもしれない |
特殊化におけるデフォルトの実装 default fn f() { ... } |
union |
モジュールまたはブロック内、識別子の前 | 共用体 union U { A(u8), B(u32) } |
'static |
生存期間 | 最小(最長)の生存期間境界 |
予約キーワード部門
予約キーワードは構文上の役割を持たないキーワードだが、将来に向けて部分的に実装されている場合がある。
キーワード | 役割 |
---|---|
abstract |
|
alignof |
|
become |
|
do |
|
final |
|
macro |
宣言マクロ2.0 |
offsetof |
|
override |
|
priv |
|
proc |
|
pure |
|
sizeof |
|
typeof |
typeof式 |
unsized |
|
virtual |
|
yield |
基本型名部門
基本型名は構文上は通常の識別子であり、名前解決時に特別扱いされる。
識別子 | 役割 |
---|---|
i8 , i16 , i32 , i64 , i128 , isize |
符号つき整数 |
u8 , u16 , u32 , u64 , u128 , usize |
符号なし整数 |
f32 , f64 |
浮動小数点数 |
char |
Unicodeスカラー値 |
bool |
真理値 |
str |
UTF-8文字列 |
Prelude部門
preludeは既定で use
されているアイテムである。現在は std::prelude::v1
が使われている。
識別子 | 役割 |
---|---|
Copy |
ムーブしてもムーブ元が有効であることを示すトレイト T: Copy |
Send |
他スレッドにムーブできることを示すトレイト T: Send |
Sized |
バイト数が一定で、ポインタがthin pointerであることを示すトレイト T: ?Sized |
Sync |
参照を他スレッドにムーブできることを示すトレイト T: Sync |
Drop |
デストラクタを定義するためのトレイト impl Drop for T { fn drop(&mut self) {} } |
Fn |
&self で呼ばれるクロージャが実装するトレイト F: Fn(&[u8]) -> &u8 |
FnMut |
&mut self で呼ばれるクロージャが実装するトレイト Box<FnMut(i32) + 'a> |
FnOnce |
self で呼ばれるクロージャが実装するトレイト F: FnOnce(i32) -> T |
drop |
値をその場で破棄する関数 drop(this_variable) |
Box |
単一の所有者をもつヒープ上のデータ Box::new([1, 2, 3]) |
ToOwned |
別の型に複製できる型 s.to_owned() |
Clone |
同じ型に複製できる型 s.clone() |
PartialEq |
推移対称関係 x == y |
PartialOrd |
推移反対称関係 x < y |
Eq |
反射推移対称関係 x == y |
Ord |
全順序 x < y |
AsRef |
共有参照への明示的な変換 s.as_ref() |
AsMut |
専有参照への明示的な変換 s.as_mut_ref() |
Into |
明示的な変換 x.into::<io::Error>() |
From |
明示的な変換 (Into の逆) impl From<T> for S { ... } |
Default |
デフォルト値をもつ型 S { x: 1 .. Default::default() } |
Iterator |
イテレータ for i in v.iter() {} |
Extend |
複数要素の一括挿入 v.extend([1, 2, 3]).iter() |
IntoIterator |
イテレータに変換可能 for i in vec![1, 2, 3] {} |
DoubleEndedIterator |
両側から走査できるイテレータ for i in v.rev() {} |
ExactSizeIterator |
大きさの判明しているイテレータ v.iter().map(|&x| x + 1).len() |
Option |
NoneかSomeをとる型 fn f() -> Option<i32> { ... } |
Some |
中身があるとき let mut x = Some(0); |
None |
中身がないとき let mut x = None; |
Result |
成功したときの結果か失敗したときのエラー内容をとる型 fn read() -> Result<T, E> { ... } |
Ok |
成功 { ...; Ok(()) } |
Err |
失敗 return Err(MyError::MysteriousError) |
SliceConcatExt |
joinとconcat用 ["Chicken", "Egg"].join(" + ") |
String |
所有権のある文字列 let s = String::from("foo"); |
ToString |
to_string用 let s = 1.to_string(); |
Vec |
所有権のある可変長配列 let x = Vec::with_capacity(100); |
標準ライブラリのマクロ部門
標準ライブラリはデフォルトで #[macro_use]
されるため、以下のマクロは指示なしですぐに使うことができる。
マクロ | 役割 |
---|---|
assert! |
falseならパニックする assert!(x < n); |
assert_eq! |
等しくないならパニック assert_eq!(a + b, b + a); |
assert_ne! |
等しいならパニック assert_ne!(i, j); |
cfg! |
#[cfg()] のマクロ版。コンパイル時の条件次第でtrue/falseになる if cfg!(debug_assert) { ... } |
column! |
コンパイル時行番号 println!("{}", column!()); |
concat! |
リテラルの連結 println!("{}", concat!("foo", "bar", 10)); |
debug_assert! debug_assert_eq! debug_assert_ne! |
デバッグビルドでのみ有効化される assert*! マクロ |
env! |
コンパイル時環境変数展開 env!("PATH") |
file! |
コンパイル時ファイル名 println!("{}", file!()); |
format! |
sprintf的なやつ let s = format!("{:08x}", 12345); |
format_args! |
format! や println! の内部処理 |
include! |
#include 的なやつ include!("precompiled.rs"); |
include_bytes! |
コンパイル時に外部ファイルをバイト列リテラルに変換 let icon = include_bytes!("icon.png"); |
include_str! |
コンパイル時に外部ファイルを文字列リテラルに変換 let csv = include_str!("data.csv"); |
line! |
コンパイル時行番号 println!("{}", line!()); |
module_path! |
コンパイル時モジュールパス println!("{}", module_path!()); |
option_env! |
コンパイル時環境変数展開(ないかもしれない場合) option_env!("PATH") |
panic! |
パニックする panic!("There must be a bug!"); |
print! |
printf的なやつ print!("Hello, {}!\n", name); |
println! |
printf的なやつ+改行 print!("Hello, {}!", name); |
stringify! |
文字列化 println!("{}", stringify!(foo bar 0 1 # $ %)); |
thread_local! |
スレッドローカル変数の定義 thread_local! { static X : RefCell<Data> = RefCell::new(Data::new()); } |
try! |
try構文 ? とほぼ同じ |
unimplemented! |
パニックするが、未実装が理由のときに使うとよい |
unreachable! |
パニックするが、本来なら到達しないはずのところで使うとよい |
vec! |
可変長配列の生成 vec![1, 2, 3] vec![10; 10000] |
write! |
fprintf的なやつ write!(f, "Hello, {}!\n", name); |
writeln! |
fprintf的なやつ+改行 writeln!(f, "Hello, {}!", name); |
Rustにおける記号の役割一覧
概要: Rustにおける記号の役割をできる限り列挙する。
ASCII制御文字部門
文字 | 役割 |
---|---|
HT (水平タブ) | 空白 |
LF (ラインフィード) | 空白 文字列中の改行 シバン #! や行コメント // の終わり |
VT (垂直タブ) | 空白 |
CR (キャリッジリターン) | 空白 CRLFでLFと等価に振る舞う |
それ以外のASCII制御文字 | 用途なし |
ASCII印字可能文字部門
文字 | 役割 |
---|---|
SP (スペース) | 空白 |
! |
内部doc-comment //! , /*! 内部属性 #![] シバン #! 不等号 != 否定やビット反転 !x 否定実装 !Trait マクロ呼び出しとマクロ定義 macro! |
" |
文字列 " r" r#" バイト列 b" br" br#" |
# |
内部属性 #![] 外部属性 #[] シバン #! 生文字列/生バイト列のガード r#####" foo "##### |
$ |
宣言マクロの仮引数 $x:expr 宣言マクロの変数展開 $x 衛生的な絶対パス $crate 宣言マクロの繰り返し束縛と繰り返し展開 $($x:tt)* $($x),* |
% |
剰余 x % y x %= y |
& |
参照型 &T /&self アドレス演算子 &x 参照外しパターン &p 論理積やビットごとの論理積 x & y x &= y 短絡回路論理積 x && y |
' |
文字 ' バイト b' 生存期間 'lt |
( ) |
式・パターン・型の優先度変更 (x) タプル式・タプルパターン・タプル型 (x, y) 関数呼び出し・タプル構造体パターン・タプル構造体や列挙型のバリアントの定義・関数宣言の引数部 f(x, y) メソッド呼び出し x.f(y, z) typeof(x) クロージャトレイト用型パラメータ Fn(u8) -> u8 pub(restricted)構文 pub(self) /pub(super) /pub(crate) /pub(in somewhere) マクロ呼び出し・定義 macro!(..) 宣言マクロの腕 ( pat ) => ( tt ) 宣言マクロの繰り返し束縛と繰り返し展開 $($x:tt)* $($x),* トークンツリー |
* |
ブロックコメント /* /** /*! とその終わり */ globインポート use somewhere::*; 生ポインタ型 *const T *mut T 参照外し *x 乗算 x * y x *= y 宣言マクロの繰り返し束縛と繰り返し展開 $($x:tt)* $($x),* |
+ |
境界の併記 T: Tr1 + Tr2 + 'a Box<Tr + Send + 'a> 加算 x + y x += y 宣言マクロの繰り返し束縛と繰り返し展開 $($x:tt)+ $($x),+ |
, |
リストインポート use std::{io, cmp}; ジェネリック引数 F<T1, T2> F(T1) -> T2 where節 where T: Tr1, S: Tr2 タプル式・タプルパターン・タプル型 (x, y) 列挙型 enum E { .. , .. } 関数呼び出し・タプル構造体パターン・タプル構造体や列挙型のバリアントの定義・関数宣言の引数部 f(x, y) クロージャ |x, y| z メソッド呼び出し x.f(y, z) 配列とスライスパターン [x, y] レコード構造体や列挙型のバリアントの定義・構造体式と構造体パターン S { x, y } パターンマッチ match .. { .. , .. } |
- |
戻り値 fn() -> T |x, y| -> T { .. } 配置構文 place <- expr 指数表記 1e-3 負号演算 -x 減算 x - y x -= y |
. |
タプル・タプル構造体パターンの残り (x, y, .., z) 構造体式のデフォルト・構造体パターンの残り S { x .. y } スライスパターンの残り [x, y, .., z] 既定実装 impl T for .. {} C互換外部関数の可変長引数 fn f(x: u8, ...); 半開区間 x .. y .. y x .. .. 閉区間 x ... y フィールド x.y メソッド呼び出し x.f(y, z) 小数点 3.14 |
/ |
コメント /* */ // 除算 x / y x /= y |
0 -9 |
整数 0xF 080 0o42 0b11 浮動小数点数 0.33 識別子の一部 x0 タプル・タプル構造体のフィールド x.0 S { 0: x } |
: |
パス区切り ::x::y 関連アイテム <T as Tr>::X X::default ジェネリクス引数 T::f::<X>() 宣言マクロの仮引数 $x:expr 境界(ジェネリクス引数、スーパートレイト、where節、関連型) T<X: Clone> 型(let, const, static, 引数など) let x : T; 構造体式・パターンのフィールド S { x: y } 名前つきブロック 'a: loop { .. } |
; |
アイテムの終わり type X = u32; 文の終わり let x = 0; 宣言マクロの腕の区切り macro_rules! { .. ; .. } |
< |
ジェネリクス引数 T<X, Y> 配置構文 place <- expr 左シフト x << y x <<= y 不等号 x < y x <= y |
= |
属性 #[cfg(feature="foo")] 型引数の既定値 trait Tr<X=Self> {} 関連型束縛 Iterator<Item=i32> 等式境界 T == S T = S 関連型または型別名 type T = (); 列挙型の判別子 enum E { X = 100 } const/staticの初期化 const X : i32 = 0; let/if let/while let let x = 0; 代入 x = y 複合代入 += -= *= /= ^= &= |= >>= <<= 等号・不等号 == <= >= != パターンマッチの腕 pat => expr 宣言マクロの腕 pat => expansion |
> |
ジェネリクス引数 T<X, Y> 戻り値 fn() -> T |x, y| -> T { .. } 右シフト x >> y x >>= y 不等号 x > y x >= y パターンマッチの腕 pat => expr 宣言マクロの腕 pat => expansion |
? |
既定のSized境界の削除 T: ?Sized try構文 File::open()? |
@ |
別名パターン x @ (y, z) 廃止されたmanaged box型の記法 |
A -F a -f |
識別子の一部 Feed 16進数 0xC0FFEE b については下も参照 |
b |
バイト・バイト列 b'x' b"Foo" |
r |
生文字列・生バイト列 br##"# # #"## |
それ以外の英字 | 識別子の一部 Zoo |
[ ] |
内部属性 #![] 外部属性 #[] 配列型・スライス型 [T; 5] [T] 配列またはスライスパターン [x; 5] [x, y] 添字記法 x[i] マクロ呼び出し・定義 macro![..] 宣言マクロの腕 [ pat ] => [ tt ] トークンツリー |
\ |
エスケープ \\ \t \n \r \" \' \0 1バイトエスケープ \x7F Unicodeエスケープ \u{00007F} 改行エスケープ ( \ + 改行 + 任意個の空白) |
^ |
排他的論理和・ビットごとの排他的論理和 x ^ y x ^= y |
_ |
識別子の一部 is_some 数値リテラルの区切り文字 1_000_000_000 型推論 Vec<_> 束縛しないパターン (_, x) |
` |
未使用 (字句解析されない) |
{ } |
リストインポート use std::{io, cmp}; 構造体、共用体、列挙型のバリアント、列挙型、トレイト、実装、モジュール、外部アイテム trait X { ... } ブロック (関数定義、制御構造) loop { ... } パターンマッチ 構造体式・構造体パターン マクロ呼び出し・定義 macro!{..} 宣言マクロの腕 { pat } => { tt } トークンツリー |
| |
パターンの並列 pat1 | pat2 => expr クロージャ |x, y| z 論理和・ビットごとの論理和 x | y x |= y 短絡回路論理和 x || y |
~ |
廃止されたowned box型の記法 現在は未使用 |
Unicode制御文字・特殊文字部門
文字 | 役割 |
---|---|
NEL, LRM, RLM, LINE SEPARATOR, PARAGRAPH SEPARATOR | 空白 |
Unicode印字可能文字部門
XID_Start
, XID_Continue
属性のついている文字は識別子の一部と解釈される。
FnBoxについて
Rustの FnBox
について、動機・仕組み・問題点を説明する。
FnBox
の動機
以前の記事では、「「クロージャを boxせずに 返したい」という欲求は人類の四大欲求のひとつと言われている。 」と書いたが、出所の異なるクロージャを同じ型で扱う必要がある場合は無理せず Box
に入れるべきである。
例えば、非同期処理など、継続渡し形式で何らかの処理をする場合は、一般に生のクロージャでは使い勝手が悪い。おもちゃ的な例として、フィボナッチ数列の計算を継続渡し形式で行うことを考える。以下のように書くとうまくいかない。
fn fib_cont<T, F: Fn(u32) -> T>(n: u32, continuation: F) -> T { if n <= 1 { continuation(n) } else { fib_cont(n - 1, |a| { fib_cont(n - 2, |b| { continuation(a + b) }) }) } } fn main() { fib_cont(13, |a| { println!("{}", a); }); }
そこで、生のクロージャではなく、トレイトオブジェクトを渡すようにする。動的ディスパッチになってしまうが、異なるクロージャを同じ型で渡せるようになる。
fn fib_cont<'a, T>(n: u32, continuation: Box<Fn(u32) -> T + 'a>) -> T { if n <= 1 { continuation(n) } else { fib_cont(n - 1, Box::new(|a| { fib_cont(n - 2, Box::new(|b| { continuation(a + b) })) })) } } fn main() { fib_cont(13, Box::new(|a| { println!("{}", a); })); }
ここで Fn
を使ったが、場合によっては FnMut
や FnOnce
をインターフェースとして使いたい場合もある。 Fn
/FnMut
/FnOnce
は以下のような違いがある。
Fn
は所有権や書き込み権限がない参照に対しても呼ぶことができる。その代わり、キャプチャーした変数に書き込めないし、仮にmoveキャプチャーでもその変数を外に動かすことはできない。FnMut
は所有権がなくても呼べるが、mut
である必要はある。キャプチャーした変数には書き込めるが、仮にmoveキャプチャーでもその変数を外に動かすことはできない。FnOnce
は所有権があるときに1回だけ呼べる。その代わり、キャプチャーした変数には書き込めるし、moveキャプチャーならキャプチャーした変数を外に動かすことができる。
実践的な例ではないが、先ほどのフィボナッチ数列の例を FnOnce
で書き直してみる。
fn fib_cont<'a, T>(n: u32, continuation: Box<FnOnce(u32) -> T + 'a>) -> T { if n <= 1 { continuation(n) } else { fib_cont(n - 1, Box::new(move |a| { fib_cont(n - 2, Box::new(move |b| { continuation(a + b) })) })) } } fn main() { fib_cont(13, Box::new(|a| { println!("{}", a); })); }
ところがこのプログラムは現在のRustではエラーになる。これは FnOnce
の定義を見るとわかる。
pub trait FnOnce<Args> { type Output; extern "rust-call" fn call_once(self, args: Args) -> Self::Output; }
この call_once
メソッドは Self
型の引数をとるにも関わらず、 Self: Sized
を要求していない。以前の記事で指摘したように、このことは実は許されているのであった。また、オブジェクト安全性の条件も実は満たされているため、 FnOnce(u32) -> T
の対応するトレイトオブジェクト自体は実際に存在することになる。
しかし実際に FnOnce(u32) -> T
を呼ぶためには、このオブジェクトをムーブで渡す必要がある。しかしムーブで渡すにはその型が Sized
である必要があるから、うまくいかない。つまりオブジェクトを生成する時点では問題にはならないが、実際にメソッドを呼ぶ段階で問題が発生することになる。
FnBox
の仕組み
std::ops
で定義されている Fn
, FnMut
, FnOnce
とは異なり、 FnBox
は単なるユーザー定義のトレイトである。 (extern "rust-call"
と丸括弧記法を採用しているという特殊性はあるものの)
そしてその定義は以下のようになっている。
pub trait FnBox<A> { type Output; fn call_box(self: Box<Self>, args: A) -> Self::Output; }
self
が Box<Self>
型をとれることは以前の記事で説明してある。
さて、このトレイトの定義は FnOnce
とほぼ同じである。唯一の違いは、 call_once
の self
が Self
なのに対して、 call_box
の self
が Box<Self>
であるという点である。したがってこの2つのトレイトは、 Self: Sized
である限り、相互に実装可能である。実際、 FnBox
は全ての FnOnce
に対して実装されている。 (もちろん逆はない。一貫性条件に反するからである。)
impl<A, F> FnBox<A> for F where F: FnOnce<A> { ... }
ここで重要なのは、 Box<FnOnce>
と Box<FnBox>
の違いである。以前の記事で説明したvtableの構造 によると、これらのfatポインタの指すvtableは以下の構造を持つことになる。
Box<FnOnce>
のfatポインタの指す先には、- 0番目: Drop Glue
- 1番目: 元の型のバイト数
- 2番目: 元の型のアラインメント
- 3番目:
call_once
へのポインタ
Box<FnBox>
のfatポインタの指す先には、- 0番目: Drop Glue
- 1番目: 元の型のバイト数
- 2番目: 元の型のアラインメント
- 3番目:
call_box
へのポインタ
さて、このfatポインタが判明している状態で、クロージャを呼ぶことを考える。 call_once
を呼ぶことを考えると、これはほぼ不可能であると考えられる。例えば f64
と u64
は通常同じバイト数、同じアラインメントを持つが、 f64
と u64
をそれぞれ第1引数に持つ関数は格納先のレジスタが異なるかもしれない。これはvtableだけからは区別できない。
一方 call_box
は FnMut::call_mut
や Fn::call
と同じように簡単である。 第1引数はthinポインタなので、fatポインタの0番目をそのまま渡せばよい。
最後に、 FnBox
は FnOnce
を実装していないが、 Box<FnBox>
は FnOnce
を実装している。
impl<'a, A, R> FnOnce<A> for Box<FnBox<A, Output = R> + 'a> { ... }
言語処理系は FnBox
を特別扱いしないが、上のように FnOnce
を実装しているため通常の関数呼び出し構文が利用可能になっている。
結局、 FnOnce
から出発して、 FnBox
を経由して FnOnce
に戻ってきたことになる。このように、 FnBox
は適切なvtableを提供するための中間表現であると言える。
FnBox
の使い方
FnBox
の使い方は簡単で、 Box<FnOnce>
として使いたい場所を Box<FnBox>
に変えるだけでよい。ただし、以下の2点に注意する必要がある。
- 機能ゲートで守られているので、nightlyコンパイラを使い、冒頭で
#![feature(fnbox)]
を宣言する必要がある。 - preludeには入っていないので、
use std::boxed::FnBox;
する必要がある。
#![feature(fnbox)] use std::boxed::FnBox; fn fib_cont<'a, T>(n: u32, continuation: Box<FnOnce(u32) -> T + 'a>) -> T { if n <= 1 { continuation(n) } else { fib_cont(n - 1, Box::new(move |a| { fib_cont(n - 2, Box::new(move |b| { continuation(a + b) })) })) } } fn main() { fib_cont(13, Box::new(|a| { println!("{}", a); })); }
安定化していない理由
FnBox
は以上のようによくできた仕組みだが、安定化されていない。 追跡用のissueによると、これは主に次の2つの理由によるもののようだ。
- 「値渡しのself」に取って代わられる可能性がある。
- トレイト実装の一貫性に引っかかる部分の解決ができていない。
- 高階トレイト境界とうまく相互作用しない。
「値渡しのself」
By-value-self (「値渡しのself」) とはまさに上で述べた FnOnce
のような状況を指している。
現段階で有効な追跡issueが見当たらなかったので、現在は誰も動いていないようだが、意図されているのは次のような内容だと思われる: selfを値渡しするメソッドがあった場合は、vtableにはサイズが不明でも渡せるような別の関数へのポインタを入れておく。selfの値渡しメソッドの呼び出しが実際に発生した場合は、それがBoxの参照外しとセットだったら Sized
でなくても許可するようにする。このようにして FnOnce
に限らない一般のトレイトに対してもワークアラウンドなしにトレイトオブジェクトが動作するようにする。
もしこの機能が本当に計画されているのであれば、 FnBox
はやがて不要になり、廃止されることになる。これが安定化されていない第一の理由である。
ただし、2015年12月の時点で、値渡しのselfまでの一時的な措置として安定化させることが決定されているため、現在はこの理由は有効ではない。
トレイト実装の一貫性
Rustでは、複数のトレイト実装が非決定的に選択可能な状況を避けるための「一貫性(coherence)」規則を採用している。これはorphan ruleとoverlapping ruleからなる。
クロージャーの実装は難しい一貫性の問題を抱えている。というのもクロージャは Fn
, FnMut
, FnOnce
と3段階に分けて提供されているため、ラッパーに対する Fn*
実装を提供するとほぼ必ず複数の経路での実装が存在することになってしまうのである。例えば、
impl<A, F: ?Sized> Fn<A> for Box<F> where F: Fn<A> { ... } // 実装A impl<A, F: ?Sized> FnMut<A> for Box<F> where F: FnMut<A> { ... } // 実装B impl<A, F: ?Sized> FnOnce<A> for Box<F> where F: FnOnce<A> { ... } // 実装C
という3種類のラッパーを提供するのはごく自然である。しかし3種類全てを提供すると、一貫性の問題が発生する。例えば F: Fn<A>
だとして、 Box<F>: FnOnce<A>
を解決したいとする。すると以下の3種類の経路の実装が選択可能である。
F: Fn<A>
と実装Aから、Box<F>: Fn<A>
が提供される。このスーパートレイトとしてBox<F>: FnOnce<A>
が導出される。F: Fn<A>
のスーパートレイトF: FnMut<A>
と実装Bから、Box<F>: FnMut<A>
が提供される。このスーパートレイトとしてBox<F>: FnOnce<A>
が導出される。F: Fn<A>
のスーパートレイトF: FnOnce<A>
と実装Cから、Box<F>: FnOnce<A>
が提供される。
Rustはこれらの同一性を確かめる術は今のところないから、そもそも上記の実装A,B,Cの共存を認めていない。
これは FnBox
に関係なく Fn
, FnMut
, FnOnce
, ひいてはより一般のトレイト (Into
など) に広く認められている問題である。ただ FnBox
も同じ問題を抱えている。というのも、
impl<'a, A> FnOnce<A> for Box<FnBox<A> + 'a> { ... } impl<A, F: ?Sized> FnOnce<A> for Box<F> where F: FnOnce<A> { ... }
がoverlapping ruleに引っかかるからである。
これは本来overlapしないが、何かのバグでそうなっているようだ。(例えば、 A
引数を削除した例を作って実験するとコンパイルが通る。)
高階トレイト境界との相互作用
また、 FnBox
は高階トレイト境界に対してうまく動作しないという問題がある。
例えば Box<FnBox(&i32)>
は Box<for<'a> FnBox(&'a i32)>
の構文糖衣だが、このように高階トレイト境界を含んでいる場合は FnOnce
が実装されないため、クロージャを呼び出すことができない。
これは一般に、高階の生存期間束縛を含む型に対するジェネリックな実装をうまく与える手段がないことに由来する。
まとめ
FnOnce
をbox化して呼び出す手段は必要性は認識されているものの、安定的な実現はまだ得られていない。どうしても今必要なら、上に挙げた問題点を把握しつつ、 std::boxed::FnBox
を使うのが望ましいだろう。
Rustで引数型と戻り値型がSizedでなくてもよい条件
過去の記事でSized
について説明したが、関数の引数と戻り値についてはやや直感に反する条件が適用されているので説明する。
- 戻り値型: 関数やメソッドが本体をもつ場合に検査される。本体を持たない場合は
Sized
でなくてもよい。 - 引数型: それ自体は検査されない。関数やメソッドが本体を持つ場合は、引数を受け取るパターンに対する型検査が実行される。このとき、値が名前に束縛されていれば、束縛される名前の型が
Sized
でなければならない。
例
trait Foo { fn f(x: str) -> str; // OK // fn g(x: str) -> str { x } // error } fn h(_: str) {} // OK // fn i(x: str) -> str { x } // error fn j(ref x : str) {} // OK, but no way to call it fn main() { }
この規則は一見すると重箱的な問題のように見えるが、実は FnOnce
トレイトを見ると実際に活用されていることがわかる。
pub trait FnOnce<Args> { type Output; extern "rust-call" fn call_once(self, args: Args) -> Self::Output; // ^^^^ Self : ?Sized }
トレイトの Self
引数だけは暗黙の Sized
境界が適用されないため、ここでは Self: ?Sized
である。(関連型は Sized
が適用される。)
この関数に Self: Sized
境界が必要ないのは、上記の条件のためである。
RustのUnsafeCellとFreeze
概要: UnsafeCell
の存在目的について説明する。
UnsafeCell
とは
UnsafeCell
とは、その名前の通り、内部可変性(interior mutability)を実現するためのunsafeはプリミティブである。
UnsafeCell
の代表的な用途は内部可変性を実現するための安全なラッパーであるCell
や RefCell
である。それらの他に、並行性を扱うモジュール(std::sync
, std::thread
, std::sys
)の内部で使われている。
UnsafeCell
自体は以下のように単なるラッパー構造体である。
pub struct UnsafeCell<T: ?Sized> { value: T, }
したがって UnsafeCell
の特異性はコンパイラによる特殊な扱いに由来することになる。
UnsafeCell
の扱い
UnsafeCell
は主に以下の点で特殊である。
T
に対して非変である。 (cf.PhantomData
は共変)- これをメンバとして含む型は
Freeze
フラグが外される。
Freeze
は core::marker
内に定義されているプライベートなマーカートレイトである。その定義は以下のようになっている。(1.17.0時点では Freeze
はまだ存在せず、コンパイラに組込みのフラグで管理されている。)
#[lang = "freeze"] unsafe trait Freeze {} unsafe impl Freeze for .. {} impl<T: ?Sized> !Freeze for UnsafeCell<T> {} unsafe impl<T: ?Sized> Freeze for PhantomData<T> {} unsafe impl<T: ?Sized> Freeze for *const T {} unsafe impl<T: ?Sized> Freeze for *mut T {} unsafe impl<'a, T: ?Sized> Freeze for &'a T {} unsafe impl<'a, T: ?Sized> Freeze for &'a mut T {}
impl Tr for .. {}
については過去の記事を参照。これにより、Freeze
はおよそ「UnsafeCell
をメンバとして持たない型」として定義されることになる。 ([T; 0]
という例外はある。)
Freeze
の扱い
Freeze
は主に次のような役割をもつ。
static
アイテムがmut
ではなく、かつ型がFreeze
である場合、このアイテムは.rodata
などの読み取り専用セクションに配置される場合がある。- 参照が
mut
ではなく、かつ参照先の型がFreeze
である場合、この参照は「エイリアスを持たない」かつ「読み取り専用」という情報がLLVMに渡される。
なお、「エイリアスを持たない」というのは正確にいうと全く正しくないが、LLVMの最適化フラグとしてのnoaliasは、「エイリアスによるデータ競合が発生しない」という意味であるため、問題ない。
逆に、真にエイリアスを持たないはずの &mut
にはnoaliasがついていない。これはLLVMの最適化バグへのワークアラウンドによるものである。
まとめ
mut
がついていないのにメモリ領域が書き換えられる可能性がある場合は、該当部分を UnsafeCell
でラップする必要がある。通常は UnsafeCell
は Cell
や RefCell
などのより高級なライブラリを経由して呼び出されるので、この内容をより具体的に把握する必要はない。