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> を使うのが適当であるといえます。そうすることで余計な所有関係や自動トレイトの継承制約などが追加されることを防ぐことができます。