Rustのパニック機構

概要: Rustのパニック機構は異常終了の処理を安全に行うためにある。この動作を詳しくみていく。

パニックとは何か

Rustには2つの異なる例外処理機構があります。

発生源 対処方法
パニック プログラミングエラー 原則として捕捉しない assert!()
境界外参照
Result 例外的な入力 必要に応じて捕捉 I/Oエラー (File::read)
パースエラー (str::parse)

パニックとResultの関係についてはTRPL第2版第9章、未定義動作とパニックの関係についてはRustonomiconのUnwindingの章などが参考になります。

パニックを想定した安全性

Rustではたとえパニック状態でも未定義動作だけは絶対に避ける必要があります。そのため以下の関数は不健全 (unsound)です

use std::ptr;
// この関数はRustではunsound (やってはいけない)
pub fn replace_with<T, F>(x: &mut T, f: F) where F: FnOnce(T) -> T {
    unsafe {
        ptr::write(x, f(ptr::read(x)));
    }
}

このコードは x から一時的に所有権を取り出し、 f にかけてから x に書き戻します。これは一見すると安全ですが、 f がパニックしたときに x が無効な値を指した状態で返されてしまいます。どうしてもこのような関数を用意したい場合には、関数自体をunsafeにして、規約を明記する必要があります。

use std::ptr;
/// `x` の値を `f` で変換します。
///
/// # 安全性
///
/// `f` がパニックした場合、 `x` の指すメモリ領域は未定義になります。
///
/// 呼び出し元は、 `f` がパニックしないよう担保するか、 `f` がパニックした
/// 場合に二重dropが発生しないように追加の処理をする必要があります。
pub unsafe fn replace_with<T, F>(x: &mut T, f: F) where F: FnOnce(T) -> T {
    ptr::write(x, f(ptr::read(x)));
}

二重パニックの抑止

Rustでは二重パニックは特に気をつけて避ける必要があります。Rustのパニックは「原則として捕捉しない」だけで実際には捕捉可能ですが、二重パニックの捕捉はできません。そのため二重パニックは通常のパニックに比べてデバッグが難しくなる可能性があります。

二重パニックは主に誤った drop 実装によって発生しえます。単純化された例では次のようなものが考えられます。

use std::ops::Drop;

struct A;
impl Drop for A {
    fn drop(&mut self) {
        println!("drop(A)");
        panic!("drop(A)");
    }
}

fn main() {
    let _x = A;
    assert_eq!(0, 1);
}

このように Drop::drop 内ではパニックを使うことができませんが、戻り値が () であることから Result を用いたエラー通知もできません。つまり、 Drop::drop 内で発生したエラーを適切に通知する手段はありません。これが困る例として BufWriter があります。

#![feature(termination_trait)] // `io::Result` を main の戻り値として使う

use std::io::{self, BufWriter, Write};
use std::fs::File;

fn main() -> io::Result<()> {
    let mut f = BufWriter::new(File::create("foo.txt")?);
    writeln!(f, "hoge")?;
    f.flush()?; // 明示的にflushしないとエラーを捕捉できない
    Ok(())
}

BufWriter はデータをすぐに書き込まずバッファに蓄えるため、dropされた時点で自動的に残りを書き込みます。しかしこのとき発生したエラーを通知する手段はありません。たとえば、上の例でファイル名を /dev/full にして実行するとエラーが表示されますが、 f.flush()? を消すとエラーは握り潰されてしまいます。

不変条件に気をつける

データに対して、常に保たれていてほしい性質のことを不変条件といいます。たとえば、何らかの理由で単調増加な配列を扱いたい場合、この「単調である」という性質は不変条件の一種です。

ところで不変条件はプログラムの実行中に一時的に壊れる場合があります。たとえば以下の処理を考えます。

// 単調増加な配列に2を足す
fn plus2(a: &mut [i32]) {
    for x in a {
        *x += 2;
    }
}

fn main() {
    let mut a = [3, 5, 6];
    plus2(&mut a);
    assert_eq!(&a[..], &[5, 7, 8]);
}

この plus2 の前後で「単調増加である」という性質は保たれます。ところが、この plus2実行中にはこの不変条件は一時的に壊れています。2項目目まで処理し終えたときの配列は [5, 7, 6] ですから単調増加ではありません。

Rustのパニック機構では、このような論理的な不変条件が壊れる可能性がありますが、その影響が極力波及しないような工夫があります。例えば、先ほどの「単調増加である」という性質を再び考え、以下のようなプログラムを考えます。

// 単調増加な配列を三乗する
fn cube(a: &mut [i32]) {
    for x in a {
        *x = x.pow(3);
    }
}

fn main() {
    let mut a = [3, 5, 6];
    cube(&mut a);
    assert_eq!(&a[..], &[27, 125, 216]);
}

このプログラムに [1290, 1291, 1292] という配列を渡すと、2番目の処理中にオーバーフローでパニックが発生します(デバッグビルドの場合)。このパニック発生時点での配列の中身は [2146689000, 1291, 1292] なので、不変条件が壊れています。しかしこの場合は、パニックを捕捉していないので、不変条件が壊れたデータをユーザープログラムが目にすることはありません。

不変条件が壊れたデータをプログラムが目にする機会には次の2つがあり、どちらも防止策があります。

  • スレッド内でパニックが捕捉された場合。これは UnwindSafe トレイトによって抑止されている。
  • 他のスレッドがデータを参照した場合。これは Mutex/RwLock がもつpoisoningの仕組みによって抑止されている。

なお、どちらの仕組みも転落防止柵くらいの簡易なもので、簡単にオプトアウトできるようになっています。Rustでは論理不変条件の保護を徹底する気はなく、たとえ論理不変条件が壊れていても安全性不変条件さえ守られていれば未定義動作が起こらないようにしなければなりません。

UnwindSafe トレイト

UnwindSafe トレイトは、後述する catch_unwind を使うさいに、不変条件が壊れたデータが漏れないようにする仕組みです。

たとえば、次のように catch_unwind を使うとpanicの巻き戻し処理を一時中断することができます。

use std::panic::{catch_unwind, resume_unwind};

// 単調増加な配列を三乗する
fn cube(a: &mut [i32]) {
    for x in a {
        *x = x.pow(3);
    }
}

fn main() {
    let result = catch_unwind(|| {
        let mut a = [1290, 1291, 1292];
        cube(&mut a);
    });
    
    // 上の処理がpanicしても実行される
    println!("do something");
    
    // panic処理をレジュームする
    result.unwrap_or_else(|e| resume_unwind(e));
}

ここで aクロージャの外に出せば、壊れた a を観測できそうですが、 &mut T: !UnwindSafe であることからコンパイルエラーになります。

use std::panic::{catch_unwind, resume_unwind};

// 単調増加な配列を三乗する
fn cube(a: &mut [i32]) {
    for x in a {
        *x = x.pow(3);
    }
}

fn main() {
    let mut a = [1290, 1291, 1292];
    // ここでコンパイルエラーになる
    let result = catch_unwind(|| {
        cube(&mut a);
    });

    // 上の処理がpanicしても実行される
    println!("a = {:?}", a);

    // panic処理をレジュームする
    result.unwrap_or_else(|e| resume_unwind(e));
}

かわりに AssertUnwindSafe(&mut a) をキャプチャーするようにすればこの仕組みをオプトアウトできます。

use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};

// 単調増加な配列を三乗する
fn cube(a: &mut [i32]) {
    for x in a {
        *x = x.pow(3);
    }
}

fn main() {
    let mut a = [1290, 1291, 1292];
    let result = {
        // &mut a の壊れた値を観測するためにAssertUnwindSafeする
        let aref = AssertUnwindSafe(&mut a);
        catch_unwind(move || {
            cube(aref.0);
        })
    };

    // 上の処理がpanicしても実行される (a = [2146689000, 1291, 1292])
    println!("a = {:?}", a);

    // panic処理をレジュームする
    result.unwrap_or_else(|e| resume_unwind(e));
}

Mutex, RwLock のpoisoning

Mutex のロックや RwLock の書き込みロックを持ったスレッドがパニックすると、その途中の値は不変条件が壊れている可能性があります。これを他のスレッドが間違って読まないようにするため、 MutexRwLock はロック(書き込みロック)を持ったスレッドがパニックした場合、poisonedフラグが立てられ、次回以降のロックが失敗するようになります。

poisonedフラグを元に戻す方法はないようですが(多分)、poisonedフラグによりエラーになった場合、返されたエラーから強制的に壊れた値を読みに行くことはできるようになっています。

パニックの流れ

ここまで、パニックに関連する基本的な注意事項をまとめました。ここからはパニックの仕組みを調べていきます。

まず、ユーザープログラムから見たパニックの流れを説明します。パニックはスレッドごとに処理されます。各スレッドの正常系をここでは次のような図であらわすことにします:

f:id:qnighy:20180218213528p:plain

パニック処理の基本的な流れ (-C panic=unwind) は以下のようになります。

f:id:qnighy:20180218213656p:plain

ただし、 -C panic=abort が指定されているときは以下のようになります。

f:id:qnighy:20180218213741p:plain

パニックハンドラ

パニックハンドラはパニック時にまず呼ばれる処理で、主にスタックトレースの表示などを担当します。デフォルトの処理は以下のようになっています。

  • もしパニックカウントが2なら、常にフルのバックトレースを表示する。
  • パニックカウントが1なら、 RUST_BACKTRACE の値に応じて以下の処理をする。
    • 0 または定義されていないならバックトレースを表示しない。
    • full ならばフルのバックトレースを表示する。
    • それ以外(1など)ならシンプルなバックトレースを表示する。
  • Box<Any + Send>&'static str もしくは String だったときは、これをエラーメッセージとして表示する。それ以外のオブジェクトだったときは、 "Box<Any>" とだけ表示する。

std::panic::set_hook を用いてパニックハンドラを設定できます。なお、パニックハンドラ自体は全スレッドで共通です。

巻き戻し

巻き戻しはLLVMの例外機構 (C++の例外にも使われている) を用いて実装されています。プラットフォームごとにGCC互換やSEHなどとして実装されるようです。

巻き戻しでは基本的にスタックトレースに沿って drop が呼ばれていきます。したがって最終的に以下のいずれかになります。

  • スレッドの先頭まで巻き戻る
  • catch_unwind の呼び出しにぶつかる
  • drop が再びパニックする (二重パニック)

スレッドの先頭まで巻き戻った場合、そのスレッドだけが異常終了したとみなされ、呼び出し元スレッドからの JoinHandle::join 呼び出しに対して Err として報告されます。

catch_unwind の呼び出しにぶつかった場合、通常処理に復帰したとみなされます。しかし前述のように、パニックハンドラによって既にエラーメッセージが表示されてしまっているので、これはエラーを握り潰すのには向いていません。想定されている用途はFFIバウンダリなどで一時的に巻き戻しを止める等で、基本的に resume_unwind と対にして使います。

drop が再びパニックした場合、二重パニックになります。二重パニックの場合はパニックハンドラ呼び出し後にプロセス全体が強制終了になります。

図を見てわかるように、パニックハンドラでパニックが起こる可能性もあり、この場合は三重パニックの可能性があります。

巻き戻さない

-C panic=abort を指定すると巻き戻さずに常にプロセスを強制終了するようになります。RFC 1513に動機が説明されています。

パニック処理の場所

パニックの処理は標準ライブラリとコンパイラの複数の箇所に分散しており追跡が困難です。ここに概要をまとめておきます。

まとめ

パニック機構について説明しました。前半ではパニック機構の注意事項(Resultとの比較、安全性、二重パニック、不変条件)について説明し、後半ではパニック機構の内部構造を説明しました。

Rustの関数ポインタの落とし穴

概要: Rustの関数ポインタの落とし穴について

その1: 関数ポインタはクロージャとは異なる

これはC/C++に慣れている人には当たり前ですが、関数ポインタ型 (fn()) とクロージャ型 (Fn()) には重大な違いがあります。それは、関数ポインタは環境をキャプチャーしないということです。大雑把にいうと、

  • 関数ポインタは、ある機械語コードのアドレス
  • クロージャは、関数ポインタと、キャプチャーした環境の対

なので、関数ポインタは、ひとつのプログラムにつき原則として有限個しかないのに対し、クロージャは、キャプチャーする環境によって無限にたくさんのクロージャを作ることができます。例えば、

fn main() {
    let closures = [3, 7, 1, 5, 8, 9, 2].iter().map(|&i| {
        move |j| i + j
    }).collect::<Vec<_>>();
    println!("{}", closures[3](14));
}

というコードでは、「3を足す関数」「7を足す関数」「1を足す関数」「5を足す関数」…… のようにたくさんの「関数」を動的に生成していますが、こういうのはクロージャでないとできません。

関数ポインタは基本的には fn で定義した関数からしか作ることができません。例外として、キャプチャーしていないクロージャは関数ポインタとして使うことができます。

fn main() {
    let f : fn(i32) -> i32 = |x| x + 1;
    println!("{}", f(3));
}

その2: fn と書いたらそれ自体がポインタ型

C言語では

int (*f)(void) = getchar;

のように、関数ポインタ型には最低1個の * がつきますが、対応するRustの記法では

let f : fn() -> i32 = i32::max_value;

のように、 * が0個で関数ポインタです。したがって、

let f : *const fn() -> i32; // 二重ポインタ!

は、関数ポインタへのポインタになってしまいます。ここを間違えるとFFIで未知のsegfaultに悩まされる可能性があります。

その3: 関数の型は関数ポインタ型ではない

例えば、以下のコードはコンパイルエラーになります。

fn foo() { println!("foo"); }
fn bar() { println!("bar"); }

fn main() {
    let mut f = foo;
    if true {
        f = bar;
    }
    f();
}
   Compiling playground v0.0.1 (file:///playground)
error[E0308]: mismatched types
 --> src/main.rs:7:13
  |
7 |         f = bar;
  |             ^^^ expected fn item, found a different fn item
  |
  = note: expected type `fn() {foo}`
             found type `fn() {bar}`

error: aborting due to previous error

error: Could not compile `playground`.

ここでは fn() ではなく、 fn() {foo}fn() {bar} という型が表示されています。これは関数定義型とよばれ、関数ポインタ型とは異なります

関数ポインタ型と関数定義型のわかりやすい違いとしてバイト数が挙げられます。以下でわかるように、関数定義型は実は0バイトです。

use std::mem;
fn main() {
    println!("{}", mem::size_of_val(&main)); // 0
    println!("{}", mem::size_of_val(&(main as fn()))); // 8
}

関数定義型は、関数ごとに異なる型がついているので、それ自体は情報を持っていなくても呼び出せるようになっています。C++でいえば、各関数ごとにファンクタオブジェクトが一個割り当てられている、以下のような状態とみることができます。

void foo_funptr() {
  printf("foo!\n");
}
struct foo {
  int dummy; //C++にはZSTがないので
  // 直接呼び出し
  void operator()() const {
    printf("foo!\n");
  }
  // 関数ポインタへの変換
  void (*to_funptr())() const {
    return foo_funptr;
  }
};

関数がジェネリクスを持っている場合は、関数定義型にも対応するジェネリクスが与えられます。なお、コンパイラは便宜上 fn() {foo} のような表示をしますが、関数定義型を名指しで指定することはできません。これはクロージャ型 ([closure@src/main.rs:3:20: 3:25] などと表示される) と同様です。

いずれにせよ、通常は必要なタイミングで関数ポインタ型に自動で変換されるので、変なコンパイルエラーなどに遭遇しない限り気にする必要はないでしょう。

その4: 関数ポインタ型にはABIと安全性フラグがつけられる

特に、C/C++とのFFIをするときは、ABIを間違えないように注意が必要です。通常のRust関数は extern "Rust" fn() なのに対して、C/C++と相互呼び出しする関数は extern "C" fn() です。それぞれ fn(), extern fn() と省略できます。また unsafe をつけて unsafe fn() のようにもできます。

なお、 C++extern "C" とは異なり、Rustの extern "C" はABIを指定するだけで、マングリング規則を変更しません。マングリングを無効化するには別途 #[no_mangle] をつけます。

また、これまたC++に慣れているとわかりづらいですが、

extern "C" fn foo() {} // ここに実体がある

extern "C" {
  fn foo(); // 別の場所にある実体とリンクする
}

は意味が異なります。 extern { .. } は、実体が他の場所にあるときに使います。

NonNull安定化記念にInternerを書いてみる

概要: NonNullが安定化され、1.25.0から使えるようになる。そこでNonNullの利用例としてInternerを実装してみた。

NonNull とは

NonNull はRustにある生ポインタ型のひとつです。元々 Unique, Shared という2つの生ポインタ型でしたが、安定化を機に統合・仕様変更が行われ、 NonNull という名前になりました。 (Uniquelibcore 内部には残っていますが、安定化の予定はなくなりました。) 予定通りに進めば、 Rust 1.25.0 から使えるようになるはずです。

Rustのポインタ関連型は仕様の微妙な違いを意識して使い分ける必要があります。以下にそれを列挙しました。

分類 エイリアス 変性 所有 非0 Send Sync
&T 参照 なし※1 共変 × 継承※2 継承
&mut T 参照 なし 非変 × 継承 継承
&Cell<T> 参照 あり 非変 × × ×
Box<T> スマート
ポインタ
なし 共変 継承 継承
*const T 生ポインタ 共変 × × × ×
*mut T 生ポインタ 非変 × × × ×
NonNull<T> 生ポインタ 共変 × × ×
  • ※1 &TTUnsafeCell を含まないときだけエイリアスなしと仮定される
  • ※2 &T: SendT: Sync のときに成立

特に参照型はエイリアスに関する仮定があることに注意してください。ここでの「エイリアス: なし」の意味は、その参照の指すメモリには他からの干渉はないとして最適化してよいという意味で、関数の引数が参照だった場合に適用されます。 Box<T> も特別に、引数や戻り値で渡されたときにエイリアスなしと仮定されます。

NonNull<T> はどんなときに便利か

Rustの型システムでうまく表現できない種類の参照を表す必要がある場合に、自分で不変条件を保ちながら unsafe なコードを書くときは、「ライフタイムがついていないけど、真正な参照」というのを表現する必要がある場合があります。この手のことをするときにはNULLかもしれない *const T よりも NonNull<T> を使うほうが適切です。

Rustの型システムでうまく表現できない参照としては、双方向連結リストがよく知られています。ここでは別の例として、internerと呼ばれるデータ構造を書いてみます。これは以下のようなAPIを備えたデータ構造です。

struct Interner { .. }
impl Interner {
    pub fn new() -> Self { .. }
    // 文字列に対して一意な整数を0から順に振って返す。
    pub fn intern(&mut self, s: &str) -> usize { .. }
    // 振られた番号から文字列を復元する。なかったらpanic。
    pub fn get(&self, idx: usize) -> &str { .. }
}

Internerの設計

Internerは正引きと逆引きのためのデータ構造が必要なので、素朴に書くと次のようになります。

use std::collections::HashMap;
pub struct Interner {
    strings: Vec<String>,
    rev_map: HashMap<String, usize>,
}

しかしこの場合同じ文字列を二重に格納するので無駄があります。そこで今回は危険を承知で、 strings 側で所有しているポインタを rev_map で使い回すことにします。概念的には次のようなものを考えます。

use std::collections::HashMap;
pub struct Interner {
    strings: Vec<String>,
    rev_map: HashMap<&'strings str, usize>,
}

ちなみにこのコードは以前の記事で説明した rental crateのマクロに通すとコンパイルはできますが、 strings に対する変更ができないので意図した通りにはなりません。

ライフタイム問題を回避する

そういうわけなので rev_map のキーを生ポインタで保持することを考えます。つまり以下のようにすることを考えます。

use std::collections::HashMap;
pub struct Interner {
    strings: Vec<String>,
    rev_map: HashMap<*const str, usize>,
}

その上で、このデータ構造は以下の不変条件を満たすようにします。

  • rev_map のキーに使われているポインタは常に strings の要素である。

非ゼロ制約をつける

HashMap がどのような構造になっているかわからないので、パフォーマンス上の利点があると断言はできませんが、非ゼロであるとわかっているものは非ゼロにしたほうがいいはずです。そこで *const str ではなく NonNull を使います。

use std::collections::HashMap;
use std::ptr::NonNull;
pub struct Interner {
    strings: Vec<String>,
    rev_map: HashMap<NonNull<str>, usize>,
}

変性を考える

今回作っている Interner は型パラメータを持たないので、変性については特に考える必要はありません。一般論としては、 NonNull<T> は共変なので注意が必要ですが、Cellのように特殊なことをやっていなければ共変で問題ないことが多いです。

所有を考える

今回作っている Interner は型パラメータを持たないので、所有については特に考える必要はありません。一般論としては、自作の Drop::drop 中で drop_in_place を呼ぶ場合には、 #[may_dangle]PhantomData<T> を組み合わせて使う必要があるかもしれません。これらはdrop checkerを宥めるためのものなので、特に困っていなければ保守的に「 #[may_dangle] をつけない」としてもたいてい安全です。

Send, Sync を実装する

生ポインタはデフォルトでは Send でも Sync でもありません。しかし、今回は他のスレッドに転送したり、複数スレッドから共有参照しても特に問題なさそうなので、 SendSync を明示的に実装します。

unsafe impl Send for Interner {}
unsafe impl Sync for Interner {}

Eq, Hash, Borrow<str> を実装する

おおよそ適切なデータ構造になってきましたが、 HashMap を動作させるにはキーが Eq + Hash を実装している必要があります。また、 &str で検索するには、キーが Borrow<str> を実装している必要があります。

そこで、 NonNull<T> のラッパーを書きます。このラッパーは大変危険なことをしているので、 Interner 内部以外で使わないよう細心の注意を払う必要があります。

pub struct Interner {
    ...
    rev_map: HashMap<StrPtr, usize>,
}
use std::hash::{Hash, Hasher};
use std::borrow::Borrow;

struct StrPtr(NonNull<str>);

impl Borrow<str> for StrPtr {
    fn borrow(&self) -> &str {
        unsafe { self.0.as_ref() }
    }
}
impl Eq for StrPtr {}
impl PartialEq for StrPtr {
    fn eq(&self, rhs: &Self) -> bool {
        unsafe { str::eq(self.0.as_ref(), rhs.0.as_ref()) }
    }
}
impl Hash for StrPtr {
    fn hash<H: Hasher>(&self, hasher: &mut H) {
        unsafe { str::hash(self.0.as_ref(), hasher) }
    }
}

実装を書く

最後に intern の実装を書きます。順番に細心の注意を払います。まずハッシュから検索して、あったらそれを返します。そうでなかったら追加する必要があるので &strString にします。ポインタの値を覚えておいて、 String は正引き用のVecに突っ込んでしまいます。覚えておいたポインタを NonNull で包んで、逆引き用のHashMapに追加します。 Entry を使うと検索と追加の二度手間を短縮できるのですが、 &str で検索しつつ String で追加するという手順では Entry が使えないのでここでは使っていません。

impl Interner {
    pub fn new() -> Self {
        Interner {
            strings: Vec::new(),
            rev_map: HashMap::new(),
        }
    }
    pub fn intern(&mut self, s: &str) -> usize {
        if let Some(&idx) = self.rev_map.get(s) {
            return idx;
        }
        let idx = self.strings.len();
        let s = s.to_string();
        let s_ptr = unsafe { StrPtr(NonNull::new_unchecked(s.as_str() as *const str as *mut str)) };
        self.strings.push(s);
        self.rev_map.insert(s_ptr, idx);
        return idx;
    }
    pub fn get(&self, idx: usize) -> &str {
        &self.strings[idx]
    }
}

完成

これでできました。確かに動いていることを確認しました。

use std::hash::{Hash, Hasher};
use std::collections::HashMap;
use std::ptr::NonNull;
use std::borrow::Borrow;

pub struct Interner {
    strings: Vec<String>,
    rev_map: HashMap<StrPtr, usize>,
}
unsafe impl Send for Interner {}
unsafe impl Sync for Interner {}

impl Interner {
    pub fn new() -> Self {
        Interner {
            strings: Vec::new(),
            rev_map: HashMap::new(),
        }
    }
    pub fn intern(&mut self, s: &str) -> usize {
        if let Some(&idx) = self.rev_map.get(s) {
            return idx;
        }
        let idx = self.strings.len();
        let s = s.to_string();
        let s_ptr = unsafe { StrPtr(NonNull::new_unchecked(s.as_str() as *const str as *mut str)) };
        self.strings.push(s);
        self.rev_map.insert(s_ptr, idx);
        return idx;
    }
    pub fn get(&self, idx: usize) -> &str {
        &self.strings[idx]
    }
}

struct StrPtr(NonNull<str>);

impl Borrow<str> for StrPtr {
    fn borrow(&self) -> &str {
        unsafe { self.0.as_ref() }
    }
}
impl Eq for StrPtr {}
impl PartialEq for StrPtr {
    fn eq(&self, rhs: &Self) -> bool {
        unsafe { str::eq(self.0.as_ref(), rhs.0.as_ref()) }
    }
}
impl Hash for StrPtr {
    fn hash<H: Hasher>(&self, hasher: &mut H) {
        unsafe { str::hash(self.0.as_ref(), hasher) }
    }
}

fn main() {
    let mut i = Interner::new();
    for s in ["a", "c", "a", "b", "b", "c", "d", "a"].into_iter() {
        println!("intern({:?}) = {:?}", s, i.intern(s));
    }
}

最後にもう一押し: drop の順番

おそらく今回は問題ないですが、このようなコードを書くときには drop の順番にも気をつかう必要があります。この場合は rev_map が先にdropされたほうが安心なので、 rev_mapManuallyDrop を使います。

pub struct Interner {
    ...
    rev_map: ManuallyDrop<HashMap<StrPtr, usize>>,
}
impl Drop for Interner {
    fn drop(&mut self) {
        unsafe {
            ManuallyDrop::drop(&mut self.rev_map);
        }
    }
}

まとめ

NonNull が安定化したので、ライフタイムではちょっと痒いところに手が届かない……というときに生ポインタを使ったコードを書きやすくなりましたが、そういったコードを書くにあたっては細心の注意が必要になります。そこで実際に生ポインタを使ったコードを書きながら、どのような点に注意をしないといけないかを復習してみました。(何か他に見落としている点があったらご指摘ください。)

2018/02/08 追記: NonNullのsinceアトリビュートが1.24から1.25.0に修正されたのを記事に反映した。

安定化間近!Rustのimpl Traitを今こそ理解する

概要: impl Trait が安定化間近である。これはトレイトオブジェクトと似た用途を持つが、静的ディスパッチされSizedのまま使えるため効率的である。

impl Trait が安定化間近

Rustでは新規の機能はまずnightlyバージョンに「不安定機能 (unstable feature)」として取り入れられます。そこでの実験を経て、プログラミング言語Rustに半恒久的に導入してもよいと合意されたものだけが「安定化 (stabilize)」され、betaやstableバージョンのコンパイラでも使用できるようになります。

さて、現在 「impl Trait」と呼ばれる機能の安定化のめどがたったというアナウンスがありました。この機能は2016年夏ごろに実装され、長い間待ち望まれてきた目玉機能のひとつでしたが、ここにきてようやっと、という感じです。そこで、 impl Trait について今一度このブログで解説してみたいと思います。

impl Trait が使えると何が嬉しいのか

impl Trait は、戻り値型を隠蔽する(トレイトオブジェクトに代わる)手段を提供します。特に、クロージャイテレータ、パーサコンビネータなど、型が煩雑になりがちなものを返したいときに有効です。 impl Trait を使うことで、

  • クロージャのようにトレイトオブジェクトにより隠蔽するしかなかったケースでは、より効率的なコードを書ける可能性があります。
  • イテレータやパーサコンビネータのように隠蔽せず型を書き下しているケースでは、煩雑な型を明示しなくてよくなる可能性があります。

impl Trait を今すぐ試すには

Rust Playgroundを使う場合は、以下の手順で試すことができます。

  1. Rust Playgroundの右上にある "Nightly" を選択する。
  2. コードの先頭に #![feature(conservative_impl_trait, universal_impl_trait)] を挿入する。
  3. 以下にあるような例を書いて試す。

手元のRustで試す場合は、以下の手順が必要です。

  1. rustup install nightly でnightlyをインストールする。
  2. 以下のどちらかの方法でnightlyを有効にする。
    • cargo +nightly build のように、コマンドに毎回 +nightly をつけることで、一時的にnightlyを有効にできます。
    • 特定のディレクトリで rustup override set nightly を実行することで、そのディレクトリ内で半恒久的にnightlyを有効にできます。
  3. コードの先頭に #![feature(conservative_impl_trait, universal_impl_trait)] を挿入する。
  4. 以下にあるような例を書いて試す。

例1: クロージャを返す

与えられたクロージャを二回適用する別のクロージャを返すプログラムは、トレイトオブジェクトを使って例えば以下のように書けます。(Box<dyn Trait>Box<Trait> の新しい記法です)

#![feature(dyn_trait)]

pub fn twice<'a, T: 'a>(f: Box<dyn Fn(T) -> T + 'a>) -> Box<dyn Fn(T) -> T + 'a> {
    Box::new(move |x| f(f(x)))
}

// もう少し使いやすいバージョン
pub fn twice<'a, T: 'a, F: Fn(T) -> T + 'a>(f: F) -> Box<dyn Fn(T) -> T + 'a> {
    Box::new(move |x| f(f(x)))
}

これは、 impl Traitを使うと以下のように書けます

#![feature(conservative_impl_trait, universal_impl_trait)]

pub fn twice<T>(f: impl Fn(T) -> T) -> impl Fn(T) -> T {
    move |x| f(f(x))
}

これで無駄な Box とおさらばすることができます。

例2: イテレータを返す

イテレータを返す関数も、以下のように簡単に書くことができます。

#![feature(conservative_impl_trait, universal_impl_trait)]

// 奇数を列挙するイテレータ
fn odds() -> impl Iterator<Item=i32> {
    (0..).map(|x| x * 2 + 1)
}

fn main() {
    println!("{:?}", odds().take(10).collect::<Vec<_>>())
}

例3: パーサーコンビネータを返す

以前の記事でも紹介したパーサーコンビネーターライブラリcombineの場合、以下のようにBoxparser!を使わずに部品化することは以前から(場合によっては)可能でしたが、コンビネーターの構造が関数のシグネチャに反映されてしまうという問題がありました。

extern crate combine;
use combine::{Parser, Stream, many1};
use combine::char::{letter, spaces, Letter, Spaces};
use combine::combinator::{Many1, Skip};

fn word<I: Stream<Item = char>>() -> Skip<Many1<String, Letter<I>>, Spaces<I>> {
    many1(letter()).skip(spaces())
}

fn main() {
    println!("{:?}", word().parse("foo bar baz"));
    println!("{:?}", word().parse("012 foo bar baz"));
}

これは以下のように impl Trait を使うとすっきり抽象化することができます。

#![feature(conservative_impl_trait, universal_impl_trait)]

extern crate combine;
use combine::{Parser, Stream, many1};
use combine::char::{letter, spaces};

pub fn word<I: Stream<Item = char>>() -> impl Parser<Input = I, Output = String> {
    many1(letter()).skip(spaces())
}

fn main() {
    println!("{:?}", word().parse("foo bar baz"));
    println!("{:?}", word().parse("012 foo bar baz"));
}

impl Trait とは何か

以下、 impl Trait について詳しく説明していきます

型とトレイトは本来別物

Rustのトレイトは、C++のコンセプトやHaskellの型クラスに近いものです。型が値を分類するのに対し、トレイトは型自体を分類します。この点でJavaのインターフェースとは少し違います。(DefaultEqなどがその例です。)

しかし、トレイトから型を作る構文が2つあります。それが dyn Traitimpl Trait です。(dyn Traitdyn は省略可能) これらはどちらも、具体的な型を隠蔽して、実装しているトレイトにだけ注目するときに使いますが、dyn Traitは動的に、impl Traitは静的に解決されるという違いがあります。

dyn Trait の仕組みとデメリット

まずは見慣れた dyn Trait から説明します。 dyn Trait なんて見たことない、と思われるかもしれませんがそれもそのはず、この構文はRFC2113で変更されたばかりでまだ安定化されていません。Box<Trait>とか、トレイトオブジェクトといえば通じると思います。型とトレイトは本来別物なのに、構文からそれが見えないことが混乱のもとになっていたため、トレイトオブジェクトであることを明示するために dyn が導入されました。そのため、本記事では dyn Trait 構文を一貫して使うことにします。

dyn Trait の仕組みは、仮想関数テーブルを用いた動的ディスパッチです。 Box<T>Box<dyn Trait> に変換するとき、元の型は忘れられてしまいますが、かわりにこのポインタがfatポインタになります。つまり、 T 自体へのポインタに加えて T の仮想関数テーブルへのポインタを保持するようになります。x86-64環境なら、 Box<T> は8byteなのに対して、 Box<dyn Trait> は16byteです。データ本体に仮想関数テーブルへのポインタを置くC++とは異なり、Rustではこのようにfatポインタ内仮想関数テーブルへのポインタを置きます。

この仕組みのため、dyn Traitにはいくつかのデメリットが存在します。まず、使えるトレイトが限定されます。 dyn Trait ではfatポインタから元の型由来の情報を復元するため、&self 引数が1個もなかったり、逆に複数ある場合には呼び出せなくなってしまいます。またfatポインタを使う都合上、selfのムーブ渡しはできません。これらの条件をオブジェクト安全性といいます。

また、必ずポインタ経由になるのと、間接コール命令になるため、実行効率が悪くなる可能性があります。

impl Trait の仕組みとデメリット

dyn Traitdyn Trait という1つの型であったのに対して、 impl Trait実はそういう型があるわけではありません。これらは匿名の型を表すためのシンタックスシュガーで、 impl Trait と書くたびに別の型に翻訳されます。そのため、構文上の使える場所が限られます。

実は、この impl Trait は、場所によって2通りに翻訳されます。

引数で使われた場合 (RFC 1591)

引数位置の impl Trait は、匿名の型引数に翻訳されます。頻出例は以下のようにコールバック関数を使う場合です。例えば、

#![feature(conservative_impl_trait, universal_impl_trait)]

// コールバックに42を渡すだけの関数
fn give_42_to_callback(callback: impl Fn(i32)) {
    callback(42)
}

という関数があった場合、これは以下のように翻訳されます。

// コールバックに42を渡すだけの関数
fn give_42_to_callback<F: Fn(i32)>(callback: F) {
    callback(42)
}

このように、 impl Trait が引数で使われた場合は、 Trait を実装する匿名の型引数に置き換えられます。したがって、引数位置の impl Trait は単なるシンタックスシュガーですが、これは戻り値位置の impl Trait を理解する上でも重要な点を含んでいます。つまり、

  • 引数位置の impl Trait の型は、呼び出し側によって静的に決定される

ということです。

戻り値で使われた場合 (RFC 1522)

上に書いたことを戻り値で置き換えたものがそのまま成り立ちます。つまり、

  • 戻り値位置の impl Trait の型は、呼び出された側によって静的に決定される

ということです。この「呼び出された側によって決まる型」は存在型といいますが、このための構文はまだRustには実装されていません。ここでは、将来実装されるであろうRFC2071から記法を借用することとすると、

#![feature(conservative_impl_trait, universal_impl_trait)]

// 42を返すクロージャを返す
fn defer_42() -> (impl Fn() -> i32) {
    || 42
}

は、以下のように匿名の存在型に置き換えられると考えることができます。

#![feature(???)]

// 42を返すクロージャを返す
fn defer_42() -> Anon1 {
    || 42
}
existential type Anon1: Fn() -> i32;

Fn() -> i32 を実装する特定の型だが、その中身が何なのかは明かされないのがポイントです。このexistential typeはnewtypeパターンとも似ていますが、クロージャのような特殊な型も含められることと、手動で Fn() -> i32 を実装しなくてもよいところが特徴です。

デメリット

されこの impl Trait ですが、まず使える場所が限られるのが一つ目のデメリットです。いくつか拡張案がありますが、今回安定化される conservative_impl_traituniversal_impl_trait では、関数/メソッドでしか使うことができません。また、トレイトメソッドの戻り値には使用できません。

もう一つのデメリットとして、あくまで元の型を型システム上隠蔽しているだけなので、動的に内容を切り替えることはできません。例えば、以下のように条件に応じて異なる型のイテレータを返すコードは、 impl Trait では実現できません。

#![feature(conservative_impl_trait, universal_impl_trait)]

use std::iter;

// nの倍数を列挙 (コンパイルエラー)
fn multiples_of(n: i32) -> impl Iterator<Item=i32> {
    if n == 0 { //~ERROR if and else have incompatible types
        iter::once(0)
    } else {
        (0..).map(move |m| n * m)
    }
}

ifを動かすなどして型を揃えるか、あきらめて dyn Trait を使うのが正解です。

#![feature(dyn_trait)]

use std::iter;

// nの倍数を列挙
fn multiples_of(n: i32) -> Box<dyn Iterator<Item=i32>> {
    if n == 0 {
        Box::new(iter::once(0))
    } else {
        Box::new((0..).map(move |m| n * m))
    }
}

ただし、上の例のような単純なケースであればeitherクレイトで解決できる場合もあります。(EitherIteratorを透過する実装になっているため)

#![feature(conservative_impl_trait, universal_impl_trait)]

extern crate either;

use std::iter;
use either::{Left, Right};

// nの倍数を列挙
fn multiples_of(n: i32) -> impl Iterator<Item=i32> {
    if n == 0 {
        Left(iter::once(0))
    } else {
        Right((0..).map(move |m| n * m))
    }
}

より詳しい比較

ここから先は、 dyn Traitimpl Trait について、より詳しく比較しながら説明していきます。

書ける場所

dyn Trait は普通の型なので、型の出現する場所ならどこでも使えます。 (ただし、 Sized でないために限定される)

impl Trait は、今回安定化される範囲内では、以下の位置に出現できます。

  • 関数(通常の関数、固有実装のメソッド、トレイトのメソッド、トレイト実装のメソッド) の引数と境界の中。 (#![feature(universal_impl_trait)])
  • 関数のうち、「通常の関数」と「固有実装のメソッド」の戻り値。 (#![feature(conservative_impl_trait)])

ただし、丸括弧記法 (Fn(T) -> UTU の位置) には出現できません。また、 impl Trait の中に impl Trait をネストさせることもできません。

括弧の位置

dyn Traitimpl Trait の括弧の位置について、mini-RFC 2250 で議論中です。 &(x + y)&(Trait + Send) との一貫性を保ちつつ、使いやすく間違いにくい構文が望まれていますが、残念ながら万能な方法はなさそうです。細かい論点があって整理するのが大変ですが、結論としては以下のような妥協点で落ち着きそうです。

  • +& より弱い。つまり、 + を使うときは &(dyn Error + Send) のように括弧を入れる必要がある。
  • 同様に、 fn foo() -> impl Fn() -> (dyn Error + Send) のように Fn() -> の直後で + を使う場合も括弧が必要。
  • 上との一貫性を保つため、 fn foo() -> (impl Error + Send) の位置にも(+を使う場合は)括弧が必要。

いずれにせよ、 dyn Traitimpl Trait はどちらも新規構文で、特に構文を分ける必要はないため、この2つの間の差異はなさそうです。

トレイト境界とライフタイム境界

dyn Traitimpl Trait では、書ける境界の種類が異なります。

  • dyn Trait に書けるもの
    • ちょうど1個の主トレイト。object-safeでなければならない。必ず最初に書く。
    • 0個以上の追加トレイト。auto traitでなければならない。
    • 高々1個のライフタイム。0個の場合は省略されたとみなされる(推論方法は後述)。
  • impl Trait に書けるもの
    • 1個以上のトレイト。順番に意味はないが、最初はトレイトでなければならない。
    • 0個以上のライフタイム。

トレイトがobject-safeであるとは、以下の条件を満たしていることをいいます。

  • 直接的または間接的な出力が全て埋められている (e.g. dyn Iterator<Item=char> はOK, dyn Iterator はダメ)
  • 直接的または間接的に Self: Sized を含意していない。 (e.g. dyn Default はダメ)
  • 祖先トレイトを含む全てのメソッドがobject-safeである。メソッドがobject-safeであるとは、そのメソッドが Self: Sized を含意しているか、または以下の条件を満たしていることをいう。
    • 型パラメーターを持たない。
    • 第一引数が &self, &mut self, self: Box<Self> のいずれかである。
    • 他に Self が出現しない。 (Self::Item とかはOK)

auto trait は、名前通り auto trait で宣言されているトレイトで、 Send, Sync, UnwindSafe, RefUnwindSafe などがそれに当たります。

dyn Trait のライフタイムは大まかにいうと次のように推論されます。

  1. 明示されているときはそれが使われる。
  2. そうでないとき、 Trait に適切な Self: 'a 境界があればそれが使われる。例えば、 Box<dyn Any>Box<dyn Any + 'static> である。 (RFC 0192)
  3. そうでないとき、 dyn Trait が構文上参照で囲まれていればそれが使われる。例えば、 &'a dyn Fn()&'a (dyn Fn() + 'a) である。 (RFC 0599)
  4. そうでないとき、関数内の場合は匿名のライフタイムが割り当てられ、関数外のときは 'static が採用される。例えば、 Box<Fn()> を返す関数の戻り値は Box<Fn() + 'static> である。 (RFC 1156)

ライフタイムに関する仮定

dyn Traitimpl Trait では、ライフタイムに対する仮定は大きく異なります。

  • dyn Trait の値は全く未知の型に由来する可能性があり、ライフタイムについては書かれている境界からしか推測できません。ライフタイムを省略した場合に上記のように推論されるのはそのためです。(0個=どのような生存期間も仮定できない、となってしまうため)
  • impl Trait は「関数の型引数」と「関数のライフタイム引数のうち、impl Trait内に構文的に出現するもの」でパラメーター化されたnewtypeに過ぎないため、これらのパラメーターが生きていれば生きていることがわかります。

後者はわかりにくいので補足します。例えば、

fn foo<T, U>() -> impl Fn() { .. }

というシグネチャの場合、 impl Fn() にはライフタイム境界がついていません(dyn Traitと異なり、推論されているわけでもありません)。しかし、 dyn Trait とは異なり、この場合は impl Fn(): 'a となる十分条件が残されています。それは、 T: 'a, U: 'a となることです。(impl Fn() の中身は Anon1<T, U> のような型であるため)

同一性

上述のように、 dyn Trait は構文的に同じなら全て同じ型なのに対し、 impl Trait は出現ごとに全く異なる型になります。つまり、

fn foo1() -> Box<dyn Trait> { .. }
fn foo2() -> Box<dyn Trait> { .. }
fn foo3() -> impl Trait { .. }
fn foo4() -> impl Trait { .. }

に対して、 if true { foo1() } else { foo2() } は通りますが、 if true { foo3() } else { foo4() }コンパイルエラーになります。

impl Trait の実際の型は位置だけではなくて、その関数のジェネリクス引数にも依存します。具体的には以下のジェネリクス引数に依存します。

  • その関数の全ての型引数。
  • その関数のライフタイム引数のうちimpl Trait の境界部分に構文的に出現するもの。

以下の例を参照してください。

#![feature(conservative_impl_trait, universal_impl_trait)]

use std::fmt;

// T に依存したものは常に返せる
fn foo1<T: fmt::Debug>(x: T) -> impl fmt::Debug {
    x
}

// 'a に依存したものは返せない
fn foo2<'a>(x: &'a i32) -> impl fmt::Debug {
    x // ERROR
}
// 'a は明示的に含まれるため、 'a に依存したものも返せる
fn foo3<'a>(x: &'a i32) -> (impl fmt::Debug + 'a) {
    x
}

トレイトの透過性

dyn Traitimpl Trait も、基本的にその境界に書かれているトレイトのみを仮定できます。しかしたとえば以下のような例外があります。

  • dyn Trait: !Sized である。 impl Trait: Sized である。
  • dyn Trait: Drop である。 impl Trait: !Drop である。
  • dyn Trait は、境界に書かれていない限り、auto trait (SendSyncなど)を自動で導出することはない。一方、 impl Trait は、もとの型のauto traitを継承する。

まとめ

以下の内容を説明しました。

  • impl Trait が安定化されると何が嬉しいのか?→クロージャなど特殊な型を使うコードが効率的・簡潔に書けるようになる。それがstableバージョンのコンパイラで使えるようになる
  • impl Trait の試しかた
  • dyn Traitimpl Trait の違い: どちらも型の素性を隠して「特定のトレイトを実装している」という風に抽象化するが、 dyn Trait は動的、 impl Trait は静的な抽象化をするため、使える場面に違いがある。
  • より詳しい挙動の説明

追記 (2018/02/04): eitherクレイトを使った方法について説明しました。

Rust libstd内での特殊化の使用例 (1.23.0時点)

概要: Rust libstd内では既に特殊化が使用されているので、特定の条件を満たすことでより効率なコードが生成される。

PartialEq<[T]>

  • 最適化される処理: スライスの比較処理
    • 間接的に最適化される処理: Vec<T>, str, String の比較など
  • 条件: 内部トレイト BytewiseEquality が実装されている場合。具体的には要素型が u8, i8, u16, i16, u32, i32, u64, i64, usize, isize, char, bool の場合
  • 理由: memcmpで効率的に比較できるため。
  • ディスパッチ用トレイト: 内部トレイト SlicePartialEq

Vec<T>::from_iter, Vec<T>::extend (ムーブ)

  • 最適化される処理: TイテレータからのVec<T>の生成延長
    • 間接的に最適化される処理: iter.collect::<Vec<_>>() など
  • 条件1: unstableかつunsafeなトレイト TrustedLen が実装されている場合。これは size_hint が正確な長さを与えることをunsafeレベルで保証するトレイトである。 (ExactSizeIteratorはsafeであり、契約の内容も微妙に異なる)
    • [T], Option<T>, Result<T, E> 由来のイテレータstr.bytes(), iter::empty(), iter::once(), (n..m), (n..=m)TrustedLen である。
    • rev(), cloned(), chain(), zip(), map(), enumerate()TrustedLen を保つ。
  • 理由1: 通常の生成/延長処理では、容量を気にしながら適宜再確保をする必要があるが、イテレータの長さが判明している場合は、最初に再確保したあとはループの各ステップでは容量チェックを省略できるから。
  • 条件2: イテレータvec.into_iter() 自体だった場合
  • 理由2: 当該イテレータが一度も消費されていない場合、単にもとの vec を復元するだけでよいから。
  • ディスパッチ用トレイト: 内部トレイト SpecExtend

Vec<T>::from_iter, Vec<T>::extend (参照)

  • 最適化される処理: &TイテレータからのVec<T>の延長 (T: Copy のとき可能)
  • 条件: スライスの slice.iter() に由来するとき
  • 理由: memcpy で効率よくコピー可能で、再確保も高々1回でよく、パニックガードが要らないから。
  • ディスパッチ用トレイト: 内部トレイト SpecExtend (上と同じ)

BinaryHeap<T>::extend, LinkedList<T>::extend

  • 最適化される処理: イテレータからのBinaryHeap<T>の延長LinkedList<T>の延長
    • 間接的に最適化される処理: iter.collect::<LinkedList<_>>() など
  • 条件1: BinaryHeap<T>BinaryHeap<T> で延長する場合
  • 理由1: swapして逆向きに追加したほうが速い場合や、ヒープの再ビルドをしたほうが速い場合がある。
  • 条件2: LinkedList<T>LinkedList<T> で延長する場合
  • 理由2: 単に連結すればよい。
  • ディスパッチ用トレイト: 内部トレイト SpecExtend

vec![x; n]

  • 最適化される処理: vec![x; n] (内部的には隠し関数 alloc::vec::from_elem)
  • 条件1: 要素型が u8
  • 理由1: memset で効率的に塗れるため。
  • 条件2: 要素型が u8, i8, u16, i16, u32, i32, u64, i64, u128, i128, usize, isize, f32, f64 で値が0のとき
  • 理由2: memset で効率的に塗れるため。
  • ディスパッチ用トレイト: 内部トレイト SpecFromElem

ToString

  • 最適化される処理: to_stringによる文字列化 (一般には Display に対して実装される)
  • 条件: str, Cow<'a, str>, String のいずれかの場合
  • 理由: 文字列をコピーするだけだから。
  • ディスパッチ用トレイト: なし (ToString を直接特殊化)

iter.zip

iter.fuse

  • 最適化される処理: iter.fuse() で得られるイテレータの実装
  • 条件: unstableなトレイト FusedIterator が実装されている場合。つまり、元のイテレータが既に、「Noneを出力したら以降はずっとNone」という性質を満たしているとき。
  • 理由: 既にfusedなので、self.doneフラグをチェックする必要がなくなる。
  • ディスパッチ用トレイト: なし (Iterator, DoubleEndedIterator を直接特殊化)

Arc<[T]>::from, Rc<[T]>::from

  • 最適化される処理: &[T] から Arc<[T]>, Rc<[T]> を作る処理 (一般には T: Clone なら使える)
  • 条件: T: Copy のとき
  • 理由: memcpy で効率的に複製できるし、 clone と違ってpanicしないのでパニックガードが要らない。
  • ディスパッチ用トレイト: ArcFromSlice, RcFromSlice

PhantomDataまとめ

概要: PhantomData<T> には3つの異なる役割があり、多くの場合は PhantomData<fn() -> T> の形で使うのが無難である。

はじめに

PhantomData<T>は特殊な型で、中身を持たないにもかかわらず、型システム上は中身を持つかのように振る舞います。幽霊型 (phantom type) と関係はありますが、幽霊型そのものではないので注意が必要です。

PhantomData の基本的な使い方

幽霊型を使う場合や、何らかの理由で構造体の外部にある型を指定する必要がある場合を考えます。例えば、幽霊型で単位を区別する

// U は Meter や Miles のような型が入るとする
struct UnitFloat<U: Unit> {
    inner: f64,
}

や、外部にあるデータを参照する

struct ExtVec<T> {
    // 中身はなし
}

のような型を考えます。

これらの定義は型引数を使っていないので、以下のようにコンパイルエラーになります。

error[E0392]: parameter `U` is never used
 --> src/main.rs:2:18
  |
2 | struct UnitFloat<U: Unit> {
  |                  ^ unused type parameter
  |
  = help: consider removing `U` or using a marker such as `std::marker::PhantomData`

error[E0392]: parameter `T` is never used
 --> src/main.rs:5:15
  |
5 | struct ExtVec<T> {
  |               ^ unused type parameter
  |
  = help: consider removing `T` or using a marker such as `std::marker::PhantomData`

そのため、PhantomData と呼ばれる特殊な型を用いて、強制的に型引数を消費します。

use std::marker::PhantomData;
trait Unit {}
struct UnitFloat<U: Unit> {
    inner: f64,
    _marker: PhantomData<fn() -> U>,
}
struct ExtVec<T> {
    _marker: PhantomData<fn() -> T>,
}

ところで、この PhantomData<fn() -> U>PhantomData<U> に置き換えてもコンパイルは通ります。しかし、この2つは微妙に動作が異なります。そこで以下に挙げるように、 PhantomData の動作を理解し、適切な型を選択するのが望ましいでしょう。(特に、 unsafe と組み合わせる場合)

PhantomData の3つの役割

PhantomData には3つの役割があります:

  • 変性 (variance)
  • 所有 (owns relation)
  • 自動トレイトの制御 (auto trait opt-out)

変性 (variance)

前述のようにRustでは未使用の型引数・ライフタイム引数がコンパイルエラーになりますが、これは正確には、「双変な型引数・ライフタイム引数はコンパイルエラーになる」というように変性 (variance) を用いて説明されます。

Rustはライフタイムに関する限定的な部分型付けを採用しています。例えば、 'a : 'b ならば &'a T&'b T と自然にみなすことができます (これを &'a T <: &'b T と書く)。この部分型関係が伝搬されるとき共変 (covariant)、逆向きに伝搬されるとき反変 (contravariant)** といいます。より正確には、

  • 'a : 'b ならば T<'a> <: T<'b> のとき、 T は (そのライフタイム引数について) 共変 (covariant) である
  • 'a : 'b ならば T<'b> <: T<'a> のとき、 T は (そのライフタイム引数について) 反変 (contravariant) である
  • X <: Y ならば T<X> <: T<Y> のとき、 T は (その型引数について) 共変 (covariant) である
  • X <: Y ならば T<Y> <: T<X> のとき、 T は (その型引数について) 反変 (contravariant) である

といいます。共変性と反変性は直交するので、実際には4通りのパターンがあります。4通りで区別する場合は、共変かつ反変であるとき双変 (bivariant) といい、共変でも反変でもないとき非変 (invariant) といいます。

多くの型は共変ですが、 &mut TT について非変、 fn(T)T について反変になります。

use std::marker::PhantomData;
// check_subtyping(T, U) : TがUの部分型のときだけコンパイルが通る
macro_rules! check_subtyping {
    ($a:ty, $b:ty) => {
        // coercionによる意図しない変換を防ぐため&&をつける
        |x: &&$a| -> &&$b { x };
    }
}

fn foo<'a: 'b, 'b>() {
    // &'a _ は共変
    check_subtyping!(&'a (), &'b ());
    // check_subtyping!(&'b (), &'a ());

    // &'a mut _ は共変
    check_subtyping!(&'a mut (), &'b mut ());
    // check_subtyping!(&'b mut (), &'a mut ());

    // &T は共変
    check_subtyping!(&&'a (), &&'b ());
    // check_subtyping!(&&'b (), &&'a ());

    // &mut T は非変
    // check_subtyping!(&mut &'a (), &mut &'b ());
    // check_subtyping!(&mut &'b (), &mut &'a ());

    // PhantomData<T> は共変
    check_subtyping!(PhantomData<&'a ()>, PhantomData<&'b ()>);
    // check_subtyping!(PhantomData<&'b ()>, PhantomData<&'a ()>);
}

fn main() {}

所有 (owns relation)

所有の概念はdrop checkerで使われます。drop checkerとは、「相互参照するデータがdropされたとき、後からdropされたデータのDrop::drop()が、既にdropされたほうのデータを参照できてしまう問題」に対処するための仕組みで、特定の条件下でライフタイムに真の包含関係を強制することで、これを回避します。

drop checkerの詳しい規則については他の資料 (Rustonomiconの説明以前の記事) を参照してもらうとして、ここでは以下のことがわかっていれば十分です。

  • 基本的には、所有している型が少ないほうがよい。 (所有している型が多いと、そのぶんdrop checkerが厳しくなってしまうので)
  • ただし、unsafeな Drop 実装を持っているときは、所有関係が少なすぎると未定義動作になる可能性がある。

つまり、特にunsafeを使っていない場合は、所有している型をできるだけ少なく保つのが原則です。

ここで重要なのは、 PhantomData<T> は例外的に T を所有しているとみなされることです(これは[T; 0]についても同様です)。特に所有を明示する必要がなければ、 PhantomData<T> を直接使うのは避けたほうがよいでしょう。

自動トレイトの制御 (auto trait opt-out)

自動トレイトとは Send, Sync, UnwindSafe, RefUnwindSafe, Freeze (※コンパイラ内部トレイト) のように、型の構造に対して再帰的に自動適用されるトレイトのことです。これの適用規則は大雑把にいうと次のような感じです。

  • 明示的な実装にマッチした場合は、それが採用される。
  • 明示的な実装にマッチしなかった場合は、全てのフィールドが当該トレイトを実装していたら、全体もそれを実装しているとみなす。

ここでも PhantomData<T> は例外的に T をフィールドとして持っているとみなされることに注意が必要です。

継承リスト

以下に挙げるのは add_constraints_from_ty, dtorck_constraint_for_ty, constituent_types_for_ty から収集した変性・所有・自動トレイトの継承関係のリストです。

変性
&'a _ 共変
&'a mut _ 共変
Adt<'a> Adtの変性による
dyn Trait + 'a 共変
dyn Trait<'a> 非変
Trait<'a>::Proj 非変
変性 所有 自動トレイトの継承
&T 共変 ×
&mut T 非変 ×
*const T 共変 ×
*mut T 非変 ×
fn(T) -> _ 反変 × ×
fn() -> T 共変 × ×
[T; n] 共変
[T] 共変
(T, _) 共変
PhantomData<T> 共変
Adt<T> Adtの変性による フィールド次第 フィールド次第
<T as Trait>::Proj 非変 ※1 ※2
impl Trait<T> 非変 ※1 ※3
dyn Trait<T> 非変 ※2
  • ※1 未解決の射影型と匿名型は型仮引数と同じ扱い
  • ※2 射影型と動的型は、明示されていない限り自動トレイトは推論されない
  • ※3 匿名型の自動トレイトは、匿名型の実際の中身に依存して決まる
  • [T; n] は、 [T; 0] であっても同じように扱われる。
  • ※ ライフタイムの変性は、コンパイラ中では上記とは正反対の記述になっている。これは 'a: 'b の向きをどう解釈するかの違いにすぎない。

上の表からわかるように、単に共変であることをいいたいときは PhantomData<fn() -> T> が適切です。

PhantomData<T>[T; 0] の違い

PhantomData<T>[T; 0] は似ていますが、以下の違いがあります。

  • PhantomData<T>T: Sized でなくてもよい。
  • [T; 0] は構造体のサイズ計算に関与するため、 struct Foo([Foo; 0]); はエラーになる。 (ただしこの仕様は確定ではない)

まとめ

PhantomData<T> を使いたいときは、単に T に対して共変であることを示したいことがほとんどです。その場合、上の表からわかるように、 PhantomData<fn() -> T> を使うのが適当であるといえます。そうすることで余計な所有関係や自動トレイトの継承制約などが追加されることを防ぐことができます。

Vecと参照を同時に返す

概要: Rustでは Vec<T> とその要素への参照を同時に返すことはできないが、これを部分的に可能にするcrateはある。

やりたいこと

以下のように、 Vec<T> とその要素への参照を同時に返したい。(あるいは、こういった組を構造体に格納したい。)

fn foo() -> (Vec<i32>, &[i32]) {
    let v = vec![1, 2, 3];
    let s = &v[1..2];
    (v, s)
}

なぜできないか

Rustのライフタイムの枠組みでは、上のように(もし仮にコンパイルが通った場合に)実際に動作するパターンと、以下のように未定義動作になるパターンを体系的に区別することができない。

fn foo_invalid1() -> (Vec<i32>, &[i32]) {
    let mut v = vec![1, 2, 3];
    let s = &v[1..2];
    v.push(10); // pushによってリアロケーションが発生するとsが壊れる
    (v, s)
}
fn foo_invalid2() -> ([i32; 3], &[i32]) {
    let v = [1, 2, 3];
    let s = &v[1..2]; // vがムーブされるだけでsが壊れる
    (v, s)
}

つまり、親のムーブに対して不変な領域であることと、アロケーションが発生しないことを保証する必要がある。

ライブラリレベルでのサポート

トレイトで上記の条件を保証することで、この需要に部分的に答えるライブラリがある。著名なものとしてowning_refrentalがある。

owning_ref

[dependencies]
owning_ref = "0.3.3"
extern crate owning_ref;

use owning_ref::OwningRef;

fn foo() -> OwningRef<Vec<i32>, [i32]> {
    let v = vec![1, 2, 3];
    OwningRef::new(v).map(|v| {
        &v[1..2]
    })
}

このcrateで中心的な役割を果たしているのはowning_ref::StableAddressである (stable_deref_trait::StableDerefの別名であることに注意)。これは deref/deref_mut が常に同じポインタを返すという契約のもと unsafe になっている。より具体的には、これ自体がムーブされた場合と、 deref/deref_mut 先のオブジェクトに変更が加えられた場合に、ポインタが変化しないことが要求されている。

例えば、

  • Vec<T>, Box<T>, Rc<T>, &TStableAddress を実装している。 (アロケートされたポインタは不変のため)
  • Cell<T>, RefCell<T>StableAddress を実装していない。 (ムーブによりアドレスが変化してしまうため)
  • 標準ライブラリにはないが、 deref_mut がポインタを変更するような型の場合は StableAddress を実装してはいけない。

StableAddress の前提のもと、例えば

  • (Vec<T>, &[T]) と書きたかった部分は OwnedRef<Vec<T>, [T]> と書くことができる。
  • 同様に (Rc<T>, &mut U) と書きたければ OwnedRefMut<Rc<T>, U> と書くことができる。

その他、 StableAddress だけではカバーできないユースケースのために ToHandle, ToHandleMut, CloneStableAddress が用意されている。

  • ToHandle, ToHandleMut を実装している型に対しては OwnedHandle が使用できる。
  • CloneStableAddress を実装している型に対しては OwnedRef がcloneできる。

rental

rental の安全性保証はowning_refと同じく、 StableDeref (StableAddress) に基づいている。こちらはユーザー定義の構造体に所有者と参照を同時に入れるためのマクロを提供する。

#[macro_use]
extern crate rental;

rental! {
    // rental! の直下はmodである必要がある
    pub mod foo_struct {
        // rental! の対象となる構造体には #[rental] か #[rental_mut] をつける
        #[rental]
        pub struct VecAndSlice<T: 'static> { // T: 'static が必要
            v: Vec<T>,
            s: &'v [T], // この 'v というライフタイム名は上の v というフィールド名と一致させる
        }
        // 生成されるメソッド
        // VecAndSlice::new(Vec<T>, f: F)
        //   fにはvをDerefしたものが渡される
        // VecAndSlice::try_new(Vec<T>, f)
        //   fがResultを返すときに使う (TryNewResultが返ってくる)
        // VecAndSlice::try_new_or_drop(Vec<T>, f)
        //   try_newと似ているが、fがErrを返したらVecは解放される。
        // unsafe VecAndSlice::borrow(&self)
        // unsafe VecAndSlice::borrow_mut(&mut self)
        //   危険を承知で、ポインタを直接取得する。
        // VecAndSlice::rent(&self, f: F)
        //   fは参照を受け取る。
        // VecAndSlice::rent_mut(&self, f: F)
        //   fはmutableな参照を受け取る。
        // VecAndSlice::ref_rent(&self, f: F)
        // VecAndSlice::ref_rent_mut(&self, f: F)
        //   rent, rent_mutに似ているが、当該の参照を返したいときに使う。
        // VecAndSlice::maybe_ref_rent(&self, f: F)
        // VecAndSlice::try_ref_rent(&self, f: F)
        // VecAndSlice::maybe_ref_rent_mut(&self, f: F)
        // VecAndSlice::try_ref_rent_mut(&self, f: F)
        //   ref_rent, ref_rent_mutに似ているが、OptionやResultが返される。
        // VecAndSlice::into_head(self)
        //   参照を捨てて、Vecだけを返す。
        //
        // また、 VecAndSliceはDerefとAsRefを実装する。
    }
}

use foo_struct::VecAndSlice;

fn foo() -> VecAndSlice<i32> {
    let v = vec![1, 2, 3];
    VecAndSlice::new(v, |v| {
        &v[1..2]
    })
}

rental!マクロが生成するメソッドの一覧はここには書いていないが、を見るのが早い。

まとめ

Rustでは Vec<T> とその要素への参照を同時に返すことはできないが、これを部分的に可能にするcrateはある。

  • owning_ref は、ヒープへの所有権とその借用を同時に持つための一般的な構造体を提供する。
  • rental は、ヒープへの所有権とその借用を同時に持つ構造体を安全に定義するためのマクロを提供する。
  • stable_deref_trait は、上記2つの安全性の基礎となる特徴づけを提供する。