Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

メンバー制約

メンバー制約 'm member of ['c_1..'c_N] は、リージョン 'm が何らかの 選択リージョン 'c_i(ある i について)と等しくなければならないことを表します。 これらの制約をユーザーが表現することはできませんが、impl Trait のライフタイム捕捉ルールにより発生します。 次のような関数を考えてみましょう。

fn make(a: &'a u32, b: &'b u32) -> impl Trait<'a, 'b> { .. }

ここで、真の戻り値の型(しばしば「隠れた型」と呼ばれます)は、ライフタイム 'a または 'b の捕捉のみが許可されます。 このことは、impl Trait 戻り値の型をより明示的な形式に糖衣構文解除すると、もう少し明確に分かります。

type MakeReturn<'x, 'y> = impl Trait<'x, 'y>;
fn make(a: &'a u32, b: &'b u32) -> MakeReturn<'a, 'b> { .. }

ここでの考え方は、隠れた型は impl Trait<'x, 'y> の位置に書くことができた何らかの型でなければならない、というものです。しかし明らかに、そのような型はリージョン 'x または 'y(あるいは 'static!)しか参照できません。スコープ内にある名前はそれだけだからです。 この制限は、最終的に 'a または 'b のみにアクセスするという制約へ変換されます。なぜなら、私たちは MakeReturn<'a, 'b> を返しており、そこでは 'x'y がそれぞれ 'a'b に置き換えられているためです。

詳細な例

メンバー制約をより詳しく説明するために、make の例をもう少し詳細に書き下してみましょう。 まず、何らかのダミー trait があると仮定します。

trait Trait<'a, 'b> { }
impl<T> Trait<'_, '_> for T { }

そして、これが make 関数です(糖衣構文解除後の形式)。

type MakeReturn<'x, 'y> = impl Trait<'x, 'y>;
fn make(a: &'a u32, b: &'b u32) -> MakeReturn<'a, 'b> {
  (a, b)
}

この場合に起こることは、戻り値の型が (&'0 u32, &'1 u32) になり、ここで '0'1 は新しいリージョン変数である、ということです。 次のリージョン制約が得られます。

'0 live at {L}
'1 live at {L}
'a: '0
'b: '1
'0 member of ['a, 'b, 'static]
'1 member of ['a, 'b, 'static]

ここで「生存性集合」{L} は、関数本体のうち '0'1 が生存している部分集合に対応します。基本的には、戻り値のタプルが構築される地点から、それが返される地点までです(実際には、'0'1 はわずかに異なる生存性集合を持つ可能性がありますが、ここで説明している点にとってはあまり重要ではありません)。

'a: '0 および 'b: '1 の制約はサブタイピングから生じます。 (a, b) の値を構築するとき、それには (&'0 u32, &'1 u32) という型が割り当てられます。リージョン変数は、これらの参照のライフタイムをより短くできることを反映しています。 しかし、この値を ab から作成するためには、次が必要です。

(&'a u32, &'b u32) <: (&'0 u32, &'1 u32)

これは、結果として &'a u32 <: &'0 u32 を意味し、したがって 'a: '0 を意味します(同様に、&'b u32 <: &'1 u32、つまり 'b: '1 も意味します)。

メンバー制約を無視した場合、'0 の値は関数本体の何らかの部分集合に推論されることに注意してください(生存性制約からですが、ここではそれを明示的には書いていません)。 それが 'a になることはありません。そうなる必要がないためです。私たちは 'a: '0 という制約を持っていますが、それは単に '0 がどれだけ大きくなれるかに「上限」を設けるだけです。 私たちは可能な限り最小の値を計算するため、'0 を生存性集合と等しいままにしておくことで十分なのです。 ここでメンバー制約が登場します。

選択肢は常にライフタイムパラメータ

現時点では、メンバー制約における「選択」リージョンは常に現在の関数のライフタイムパラメータです。 2026 年 3 月時点では、これは impl Trait の配置から導かれますが、将来的にはそうでなくなる可能性があります。 現在のコードを単純化できるため、私たちはこの事実をある程度利用しています。 特に、'0 member of ['1, 'static] のようなケースを考慮する必要がありません。このケースでは '0'1 の両方の値が推論されており、したがって変化しているためです。 詳細については rust-lang/rust#61773 を参照してください。

メンバー制約の適用

メンバー制約は、他の形式の制約よりも少し複雑です。 これは、メンバー制約に「または」の性質があるためです。つまり、選択しなければならない複数の選択肢を記述します。 たとえば、例の制約 '0 member of ['a, 'b, 'static] では、'0'a'bまたは 'static と等しい可能性があります。 どのように正しいものを選べばよいのでしょうか。 現在行っていることは、最小の選択肢を探すことです。もしそれが見つかれば、'0 をその最小の選択肢と等しくなるように拡大します。 その最小の選択肢を見つけるために、下限と上限という 2 つの要因を考慮します。

下限

下限とは、'0より長く存続しなければならないライフタイム、すなわち '0 がそれより大きくなければならないものです。 実際、メンバー制約を適用する段階では、私たちはすでに '0 の下限を計算済みです。なぜなら、'0 の最小値(少なくとも、メンバー制約以外のすべてを考慮した下限)を計算しているためです。

LB'0 の現在の値とします。 すると、'0 の最終的な値が何であれ、'0: LB が成り立たなければならないことが分かります。 したがって、'choice: LB が成り立たない任意の選択肢 'choice を除外できます。

残念ながら、この例ではこれはあまり役に立ちません。 '0 の下限は単に生存性集合 {L} であり、すべてのライフタイムパラメータがその集合より長く存続することが分かっています。 したがって、ここでは同じ選択肢の集合が残ります。 (ただし他の例、特に異なる分散を持つものでは、下限制約が関連する場合があります。)

上限

上限とは、'0 より長く存続しなければならないライフタイム、すなわち '0 がそれより小さくなければならないものです。 私たちの例では、これは 'a です。なぜなら、'a: '0 という制約があるためです。 より複雑な例では、その連鎖はより間接的な場合があります。

下限と非常によく似た方法で、上限を使ってメンバーを除外できます。 UB が何らかの上限である場合、UB: '0 が成り立たなければならないことが分かります。そのため、UB: 'choice が成り立たない任意の選択肢 'choice を除外できます。

私たちの例では、選択肢集合を ['a, 'b, 'static] から ['a] のみに減らすことができます。 これは、'0 には 'a という上限があり、'a: 'b'a: 'static も成り立つことが分かっていないためです。

(実装において上限をどのように収集するかについてのメモは、下のセクションを参照してください。)

最小の選択肢

下限と上限を適用した後でも、複数の可能性が残ることがあります。 たとえば、反対の分散を持つ型を使った、私たちの例の変種を想像してください。 その場合、'a: '0 ではなく '0: 'a という制約を持つことになります。 したがって、'0 の現在の値は {L, 'a} になります。 これを下限として用いると、'b: 'a が成り立つことは分かっていないため、メンバーの選択肢を ['a, 'static] まで絞り込むことができます(ただし、'a: 'a'static: 'a は成り立ちます)。 上限は存在しないため、それが最終的な選択肢集合になります。

その場合、私たちは最小の選択肢ルールを適用します。基本的には、選択肢の 1 つが他のものより小さい場合、それを使用できます。 この場合は、'a を選ぶことになります('static ではありません)。 この選択は、リージョン伝播の一般的な「流れ」と一貫しています。リージョン伝播は常に、推論対象のリージョンについて最小値を計算することを目指すためです。 ただし、これはやや恣意的です。

実装で上限を収集する

実際には、上限を計算するのは少し不便です。なぜなら、私たちのデータ構造はその逆向きに設定されているからです。 私たちが行うのは、逆SCCグラフを計算することです(これは遅延的に行い、結果をキャッシュします)。つまり、'a: 'b が辺 SCC('b) -> SCC('a) を導くグラフです。 通常のSCCグラフと同様、これはDAGです。 その後、このグラフで SCC('0) から開始して深さ優先探索を行えます。 これにより、'0 より長く生存しなければならないすべてのSCCに到達できます。

ひとつ厄介なのは、「上限」のSCCをたどる時点では、それらの値がまだ完全には計算されていないということです。 しかし、それらの生存性制約はすでに適用済みなので、その値についてある程度の情報はあります。 特に、ライフタイムパラメーターを表す任意のリージョンについて、その値には自身が含まれます(つまり、'a の初期値には 'a が含まれ、'b の値には 'b が含まれます)。 したがって、到達可能なすべてのライフタイムパラメーターを収集できます。これはまさに、私たちが関心を持っているものです。