🎉

struct Foo<'a>(PhantomData<&'a()>)の使いどころさんを考える

2024/03/23に公開6

Rustを特徴付ける機能の一つとしてライフタイム注釈が上げられる。この機能によって、俗に言う鼻から悪魔が出てくることは無く、GCが無くても安全なメモリ管理を実行できる。

Rustの学習途上でstruct Foo<'a>(PhantomData<&'a()>)という構造体を目にしたことがあった。これが仮に例えばstruct RefStrEnvelope<'a>(&'a str)等ならまだ理解できるが、修飾すべき対象もなく何故ライフタイムだけが存在しているのか疑問をもった。今般その使いどころのようなモノが何となく理解できたのでまとめておこうかと考えこのエントリを書いた次第、何卒お付き合いの程。

traitとライフタイム

それでは実際にどのようなUse caseが存在するのか実際にコードを書きながら検証していくことにしたい。また話を単純化しているので、作為的な実装がちらほら出てくると思われるが、その点何卒ご了承の程。

前準備

前準備としてとりあえず役者を作っておく。

まず主役となる、型Tから型Uへ変換するconvert関数を持つtraitを考えてみよう。概ね以下のようになるかと思う

pub trait TypeArgConverter<T, U> {
	fn convert(scr: T) -> U;
}

pub trait AssociatedConverter {
	type Target;
	type Output;
	
	fn convert(scr: Self::Target) -> Self::Output;
}

この二つは文字通り、型引数を取るTypeArgConverterとアソシエート型を使うAssociatedConverterとなっている。ここでは型引数とアソシエート型の使い分けを議論するつもりはなく、また僕自身この領域における見解を持っているわけではないので併記しながら検証を進めていこう。ついでにネタばらしするとこの二つ書いとかないと後々詰んでしまうw

次にごく単純なKeyValuePairを定義しておこう。但しKeyをi32、ValueをStringにしたい反面、CopyCloneを持っていると少々具合が悪いのでWrapperもいっしょに定義した。

pub struct Integer(i32);

pub struct Text(String);

pub struct KeyValuePair {
	key: Integer,
	value: Text,
}

次に、先に定義したKeyValuePair参照版を作ってみる定義は以下の通り

pub struct RefKeyValuePair<'a>{
	key:&'a Integer,
	value:&'a Text
}

なんてこと無くライフタイム注釈'aを付けた参照を持っている

&KeyValuePairからRefKeyValuePairへの変換

さて、とっかかりとして単純にKeyValuePairの参照からRefKeyValuePairへの変換を行う関数を考えると以下のようになる。

fn convert(scr: &KeyValuePair) -> RefKeyValuePair<'_> {
	RefKeyValuePair {
		key: &scr.key,
		value: &scr.value,
	}
}

この場合、ライフタイムが推論されて匿名ライフタイム'_で修飾された結果を返すことになる。冗長だがしっかり書けば以下のようになる

fn strict_convert<'a>(scr: &'a KeyValuePair) -> RefKeyValuePair<'a> {
	RefKeyValuePair {
		key: &scr.key,
		value: &scr.value,
	}
}

traitとからめる

それではこの変換をTypeArgConverterで行った場合、概ね以下のような実装になる。

pub struct TypeArgConv;

impl<'a> TypeArgConverter<&'a KeyValuePair, RefKeyValuePair<'a>> for TypeArgConv {
	fn convert(scr: &'a KeyValuePair) -> RefKeyValuePair<'a> {
		RefKeyValuePair {
			key: &scr.key,
			value: &scr.value,
		}
	}
}

このとき型引数T&'a KeyValuePairとなり、型引数URefKeyValuePair<'a>となる。

型引数に具体的な型を代入した時点でライフタイムが既に注釈されているので、中身のconvert関数でも問題なくライフタイム注釈が付与された形になっていることが解る。

一筋縄でいかないAssociatedType

それでは次に、AssociatedConverterを実装していこう。素朴に考えれば以下のような実装をまず思い浮かべる

impl<'a> AssociatedConverter for TypeArgConv {
	type Target = &'a KeyValuePair;
	type Output = RefKeyValuePair<'a>;
	
	fn convert(scr: Self::Target) -> Self::Output {
		RefKeyValuePair {
			key: &scr.key,
			value: &scr.value,
		}
	}
}

ただしこれはコンパイルに失敗する。

error[E0207]: the lifetime parameter `'a` is not constrained by the impl trait, self type, or predicates
  --> src\lifetime_sample.rs:56:6
   |
56 | impl<'a> AssociatedConverter for TypeArgConv {
   |      ^^ unconstrained lifetime parameter

概略<'a>の制約先が無いよということだ。

じゃあどうすればいいかというと、最初に思い浮かぶのは、traitにライフタイム注釈を付けちまえってかんじかなと(結構作為的w

pub trait AssociatedConverter<'a> {
	type Target;
	type Output;
	
	fn convert(scr: Self::Target) -> Self::Output;
}

pub struct AssociateConv;

impl<'a> AssociatedConverter<'a> for AssociateConv {
	type Target = &'a KeyValuePair;
	type Output = RefKeyValuePair<'a>;
	
	fn convert(scr: Self::Target) -> Self::Output {
		RefKeyValuePair {
			key: &scr.key,
			value: &scr.value,
		}
	}
}

こうすれば、制約先も出来るし実装内部でも使えるので、一見うまくいったように見える。ただし、以下のような問題点も存在する

  • trait内部で'aを使ってないので冗長
  • そもそも外部定義のtraitだと詰む

前者はともかく、後者はちょっとだけ致命的である

真打ち登場

さて、じゃあどうするか考えてみよう。traitの実装には実装したいtraitとそのアソシエート先となる型が必要になる。そして先の制約は別に型にかけても構わない。

そう、その結果一見何に使えば良いのか解らないFoo<'a>(PhantomData<&'a()>)が登場するのだ。実際に書いてみよう

pub struct AssociateConv<'a>(PhantomData<&'a ()>);

impl<'a> AssociatedConverter for AssociateConv<'a> {
	type Target = &'a KeyValuePair;
	type Output = RefKeyValuePair<'a>;
	
	fn convert(scr: Self::Target) -> Self::Output {
		RefKeyValuePair {
			key: &scr.key,
			value: &scr.value,
		}
	}
}

型側にライフタイム注釈を持たせることによって、traitの不要なライフタイム注釈は不要となり、必要な時点でライフタイム注釈を定義できることになる。

まとめ

ハナから型引数使いなよという話は置いといて、アソシエーション型であったとしてもライフタイム注釈を必要とする型を定義したり、参照を定義できたりすることが出来た。

また、traitで行く当てのないライフタイム注釈を準備工事的に用意するより必要に応じて定義可能な形に決着できたのでわりかし僕の中では学びが大きかったので備忘録的にまとめておこうかと考えた次第。

ご精読ありがとう御座いました

Discussion

kanaruskanarus

( 本筋と関係ないのでどうするか迷いましたが、まとめてしまったのでとりあえず投下します... )

typo
  • 前準備

    型Tから型Uへ変換するconv関数を持つtraitを

    → …convert関数…

  • &KeyValuePairからRefKeyValuePairへの変換

    この場合、ライフタイムが推論されて匿名ライフタイム``_`で

    → … '_

  • 一筋縄でいかないAssociatedType

    pub struct AssociateConv;
    
    impl<'a> AssociatedConverter for TypeArgConv {
    

    → ( 1行目がいらない?)


「鼻から悪魔」という表現は初めて見てググりました()

時計屋時計屋

ご指摘ありがとう御座います。
早速訂正させていただきました。

タコタコ

挙げられている例では

pub struct AssociateConv<'a>(&'a ());

でも良いように思いました。

時計屋時計屋

コメントありがとうございます。

今回ライフタイム注釈しか使ってないのでたしかにUnit()を使っても良いかもしれないですね。
個人的にはランタイムには全く関与せず、コンパイル時にのみ静的に解析してほしいという意味を強調する意図も含めてPhantomDataにしたいかなと感じているところではあります。

また、コメントを頂いて、この返信を書いていたときに、こじつけに近く問題ないと僕自身も考えているとは言え、少々気になった点があることに気付きました。
それはUnit型自体が何も持ち得ないので恐らくコンパイル時に削除されるとは思うのですが、PhantomDataがその説明で

Zero-sized type used to mark things that “act like” they own a T.

と明示されている意味からもこっちを使っといた方が良いかなぁと言う雑感を持っています。(とは言えそこまで強い意志を持っているわけではなく僕はこっちを選択するけど、他の人がタコさんと同じ記述をしていたとしても別にそれはそれで構わないかな程度の差です)

kanaruskanarus

PhantomData<&'a ()> なら zero-sized なのに対して、
unit 自体は zero-sized ( https://doc.rust-lang.org/reference/type-layout.html#tuple-layout ) ですが &'a () は 1 word ( https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=aa08f71085c2a767e393f22f55432ba4 ) ですし、コンパイラとしても &'a () を勝手に潰して挙動が変わらない保証がなく「コンパイル時に削除される」ことはないはずです ( たぶん. 詳しくは rustc に詳しい人に訊けばわかるかと ) 。
個人的には素直に PhantomData を使うのがいいように思います。

タコタコ

なるほど。確かにzero-sizedが保証されているPhantomDataを使うのが良いですね。