Rust トレイトオブジェクトの生存期間境界の既定値
概要: トレイトオブジェクト型には生存期間を指定できるが、省略した場合既定値が適用される。この既定値を決定する規則について説明する。
トレイトオブジェクトの生存期間境界
トレイトオブジェクトは正確には以下の構文を持つ。
// obligation 1: only Send, Sync, or lifetimes are allowed in TraitObjectBound. // (multiple occurences of Send or Sync are allowed) // obligation 2: at most one occurence of lifetime is allowed in TraitObjectBound. TyTraitObject ::= PolyTraitRef ("+" TraitObjectBound)* "+"? TraitObjectBound ::= PolyTraitRef | Lifetime PolyTraitRef ::= ("for" "<" LifetimeParams ">")? TraitRef LifetimeParams ::= ( ) | LifetimeParam ("," LifetimeParam)* ","? LifetimeParam ::= Lifetime (":" LifetimeBounds)? LifetimeBounds ::= ( ) | Lifetime ("+" Lifetime)* "+"?
文章で説明すると、以下のようになる。
- 原則として、
Foo
のようにトレイト名を書くと、これがそのままトレイトオブジェクト型になる。 for<'a> Foo<'a>
のように、全称量化を書くことができる。引数には生存期間のみ指定できる。Foo + Send + 'a
のように、+
で複数の境界を繋ぐことができる。この時最初の要素はトレイトでなければならない。- 最初以外の要素にトレイトを指定する場合は、
Send
またはSync
を指していないといけない。それ以外のトレイトを指定した場合はエラーになる。 - 生存期間は最大1個だけ指定できる。(
Foo + 'a + 'a
のように同じものを書くのも禁止されているようだ)
例えば、行儀は良くないが、以下のようなコードが書ける。
trait Foo {} fn f<'a>(_: &(for<'b: 'a +> Foo + std::marker::Send + 'a + Send + Sync + for<'b:, 'c> Send +)) {} fn main() { }
トレイトオブジェクトの生存期間境界の既定値
上記のように、トレイトオブジェクトには生存期間を最大1個指定できるが、指定されなかった場合は既定値が適用される。
トレイトオブジェクトの生存期間境界の既定値は、Rust RFC 1156 にて (Rust RFC 0599を上書きする形で)規定されている。
Rust RFC 1156によると、これは以下の規則に基づく。
- トレイトオブジェクトの生存期間境界は、たとえ省略されていても、生存期間の省略規則(lifetime elision)の適用対象外となる。つまり生存期間の省略規則 (lifetime elision) の解決後も、未解決のまま残っている。本規則は、生存期間の省略規則 (lifetime elision) の解決後に適用される。
- 該当トレイトオブジェクトの生存期間境界は、その外側の型に応じて決定される。
- 外側が参照の場合は、その参照の生存期間が採用される。
- 外側がユーザー定義型(構造体、列挙型、共用体)の場合は、以下の規則に基づいて決定される。
- それ以外の型や、外側がない場合は、
'static
が採用される。
実際のコンパイラではastconv
の関数内 でこの処理が行われている。これを読み解くと、より正確な規則は以下のようになる。
- トレイトオブジェクトの生存期間境界は、生存期間の省略規則(lifetime elision)の適用対象外となる。 (
GatherLifetimes
の該当部分) - Rust RFC 1156に従って生存期間の解決を試み、
map
(named_region_map
) に追加する。resolve_object_lifetime_default
- 生存期間が明示されていれば、以下の処理は実行されない。 (
LifetimeContext
の該当部分) - スコープを内側から外側に走査し、
Scope::ObjectLifetimeDefault
という指示が最初に見つかったところで終了する。 - みつからなかった場合は
'static
が採用される。 Scope::ObjectLifetimeDefault
は以下の2つの位置に生成される。- 1つ目は参照の内側である。 (
LifetimeContext
の該当部分) - 2つ目はパス(識別子)で表される名前全般(構造体、共用体、列挙型、型別名、トレイト、関連型、列挙型のバリアント)の型実引数の内側である。 (
LifetimeContext
の該当部分) - 2つ目については、対応する仮引数に対する境界 (
T: 'a
)が検索される。 (object_lifetime_defaults_for_item
)- 型仮引数の位置の境界、where節の位置の境界も検索される。
- ただし、現在の実装では、なぜか、 where節に対する
for<>
で1つ以上の生存期間で量化されている場合は検索されないようだ。 where T: 'a, T: 'a
のように冗長な場合も2個と数えられる。
- 検索された境界に対して、以下のような判定が行われる。 (
LifetimeContext
の該当部分)- ひとつもないとき: 関数本体なら、この時点では生存期間を割り当てない。それ以外の文脈の場合は
'static
を割り当てる。 - ちょうど1つあるとき: その生存期間引数が採用される。
- 2つ以上あるとき: 「曖昧」とする。この時点では生存期間を割り当てない。
- ひとつもないとき: 関数本体なら、この時点では生存期間を割り当てない。それ以外の文脈の場合は
- 生存期間が明示されていれば、以下の処理は実行されない。 (
- 上記で部分的に解決された
named_region_map
を用いて、hir::TyTraitObject
をty::TyDynamic
に変換する (astconv
の関数内)- 生存期間が明示されている場合は、それを採用する。
- それ以外の場合は、該当トレイトの
Self: 'a
境界を(おそらく再帰的に)検索する。 (compute_object_lifetime_bound
)- 推測だが、これは「生存期間」ではなく「リージョン」の境界を検索している。したがって関数本体の外では
'static
以外の境界が実際には列挙されないものと思われる。 Self: 'static
境界がある場合、'static
が採用される。- それ以外に、
Self: 'a
境界がちょうど1つあるとき(Self: 'a + 'a
のような重複も1つと数える)、その生存期間が採用される。 Self: 'a
境界が2つ以上あるとき、「曖昧」でエラーとなる。Self: 'a
境界がない場合は、次の手段を試す。
- 推測だが、これは「生存期間」ではなく「リージョン」の境界を検索している。したがって関数本体の外では
- 上の方法でうまくいかない場合は、先ほど生成した
named_region_map
から解決済みの生存期間を取り出す。 - それも見つからない場合は、リージョン推論を試みる(新しいリージョン変数を生成する)。
- それもできない場合(関数本体ではなく、外側のユーザー定義型に2個以上の境界が指定されていた場合)は、エラーになる。
- 解決済みでない場合は、「関数本体内かつ、外側のユーザー定義型に境界が指定されていなかった」場合か、「外側のユーザー定義型に2個以上の境界が指定されていた」場合のいずれかである。
まとめ
トレイトオブジェクトは生存期間境界を1つ指定できるが、省略された場合は一定の規則にしたがって既定値が与えられる。既定値はRFC1156に指定されているように、原則として型の構造とその型に指定された境界によって決められるが、関数本体で使われている型についてはさらに複雑な規則が与えられている。Rustの細かい仕様を追っているといつも思うが、比較的若い言語にしてはかなり複雑な仕様(あるいはコンパイラ実装上生じてしまった複雑性)を抱えている部分が多く、綺麗な見た目と比較して驚きを覚える。