멤버 제약
멤버 제약 'm member of ['c_1..'c_N]는 영역 'm이 어떤 선택 영역 'c_i(어떤 i에 대해)와 같아야 함을 나타냅니다. 이러한 제약은 사용자가 직접 표현할 수 없지만, impl Trait의 라이프타임 캡처 규칙 때문에 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에만 접근할 수 있다는 제약으로 옮겨집니다. 'x와 'y가 각각 'a와 'b로 대체된 MakeReturn<'a, 'b>를 반환하기 때문입니다.
자세한 예제
멤버 제약을 더 자세히 설명하기 위해 make 예제를 좀 더 자세히 풀어 봅시다. 먼저, 다음과 같은 더미 트레이트가 있다고 가정합시다:
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) 타입이 할당됩니다 – 영역 변수는 이 참조들의 라이프타임이 더 작아질 수 있음을 반영합니다. 그러나 이 값을 a와 b로부터 생성하려면 다음이 반드시 성립해야 합니다:
(&'a u32, &'b u32) <: (&'0 u32, &'1 u32)
이는 결국 &'a u32 <: &'0 u32이고, 따라서 'a: '0임을 의미합니다(마찬가지로 &'b u32 <: &'1 u32, 'b: '1임을 의미합니다).
멤버 제약을 무시하면 '0의 값은 (명시적으로 적지 않은 활성 제약으로부터) 함수 본문의 어떤 부분 집합으로 추론된다는 점에 유의하십시오. '0은 결코 'a가 되지 않는데, 그럴 필요가 없기 때문입니다 – 'a: '0이라는 제약이 있기는 하지만, 이는 '0이 얼마나 크게 커질 수 있는지에 “상한“을 둘 뿐입니다. 우리는 가능한 최소 값을 계산하므로, '0을 그저 활성 집합과 같은 상태로 두어도 문제가 없습니다. 바로 이 지점에서 멤버 제약이 필요해집니다.
선택지는 항상 라이프타임 매개변수입니다
현재 멤버 제약의 “선택” 영역은 항상 현재 함수의 라이프타임 매개변수입니다. 2026년 3월 현재, 이는 impl Trait(impl 트레이트)가 놓이는 위치에서 자연스럽게 따라 나오는 결과이지만, 앞으로는 그렇지 않을 수도 있습니다. 이 사실 덕분에 현재 코드가 단순해지므로, 우리는 이를 어느 정도 활용합니다. 특히 '0과 '1의 값이 모두 추론 중이어서 변하고 있는 '0 member of ['1, 'static] 같은 경우를 고려할 필요가 없습니다. 자세한 내용은 rust-lang/rust#61773을 참조하십시오.
멤버 제약 적용하기
멤버 제약은 다른 형태의 제약보다 조금 더 복잡합니다. 멤버 제약에는 “또는“의 성격이 있기 때문입니다. 즉, 멤버 제약은 우리가 그중 하나를 골라야 하는 여러 선택지를 기술합니다. 예를 들어 예시 제약 '0 member of ['a, 'b, 'static]에서 '0은 'a, 'b, 또는 'static과 같을 수 있습니다. 올바른 것을 어떻게 고를 수 있겠습니까? 현재 우리가 하는 방식은 최소 선택지를 찾는 것입니다. 최소 선택지를 찾으면 '0을 그 최소 선택지와 같아지도록 키웁니다. 그 최소 선택지를 찾기 위해 우리는 하한과 상한이라는 두 가지 요소를 고려합니다.
하한
하한은 '0이 반드시 더 오래 살아야 하는 라이프타임, 즉 '0이 그보다 커야 하는 라이프타임입니다. 사실 멤버 제약을 적용할 시점이 되면 우리는 이미 '0의 최솟값을 계산했으므로 '0의 하한도 이미 계산해 둔 상태입니다(적어도 멤버 제약을 제외한 모든 것을 고려한 하한은 계산되어 있습니다).
LB를 '0의 현재 값이라고 합시다. 그러면 '0의 최종 값이 무엇이든 '0: LB가 반드시 성립해야 함을 알 수 있습니다. 따라서 'choice: LB가 성립하지 않는 선택지 'choice는 모두 배제할 수 있습니다.
안타깝게도 우리 예시에서는 이것이 그다지 도움이 되지 않습니다. '0의 하한은 그저 활성 집합 {L}일 것이며, 모든 라이프타임 매개변수가 그 집합보다 오래 산다는 것을 우리는 알고 있습니다. 따라서 여기서는 같은 선택지 집합이 그대로 남습니다. (하지만 다른 예시, 특히 변성이 다른 예시에서는 하한 제약이 의미가 있을 수 있습니다.)
상한
상한은 '0보다 반드시 더 오래 살아야 하는 라이프타임, 즉 '0이 그보다 작아야 하는 라이프타임입니다. 우리 예시에서는 'a: '0이라는 제약이 있으므로 상한은 'a가 됩니다. 더 복잡한 예시에서는 이 연쇄가 더 간접적일 수 있습니다.
상한을 사용하면 하한과 매우 비슷한 방식으로 멤버를 배제할 수 있습니다. 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: 'a와 'static: 'a는 성립합니다) 멤버 선택지를 ['a, 'static]으로 좁힐 수 있습니다. 상한은 전혀 없으므로 이것이 최종 선택지 집합이 됩니다.
그런 경우에는 최소 선택 규칙을 적용합니다. 간단히 말해, 선택지 중 하나가 다른 것들보다 작다면 그것을 사용할 수 있습니다. 이 경우에는 'static이 아니라 'a를 선택하게 됩니다.
이 선택은 영역 전파의 일반적인 ’흐름’과 일치합니다. 영역 전파는 항상 추론 중인 영역의 최소값을 계산하는 것을 목표로 합니다. 하지만 이 선택은 다소 임의적입니다.
구현에서 상한 수집하기
실제로는 상한을 계산하기가 다소 불편합니다. 우리의 자료 구조가 그 반대 방향에 맞게 구성되어 있기 때문입니다. 우리는 역방향 SCC 그래프를 계산합니다(이 작업은 지연 방식으로 수행하며 결과를 캐시합니다). 즉, 'a: 'b가 SCC('b) -> SCC('a) 간선을 만들어 내는 그래프입니다. 일반 SCC 그래프와 마찬가지로 이 그래프도 DAG입니다. 그런 다음 이 그래프에서 SCC('0)부터 시작하는 깊이 우선 탐색을 수행할 수 있습니다. 이를 통해 '0보다 오래 살아야 하는 모든 SCC에 도달하게 됩니다.
한 가지 까다로운 점은 “상한” SCC들을 순회하는 시점에 그 값들이 아직 완전히 계산되지 않았다는 것입니다. 하지만 그 SCC들의 활성 제약 조건은 이미 적용했으므로, 그 값에 대한 정보를 어느 정도 가지고 있습니다. 특히 라이프타임 매개변수를 나타내는 영역이라면 그 값에는 자기 자신이 포함됩니다(즉, 'a의 초기값에는 'a가 포함되고 'b의 값에는 'b가 포함됩니다). 따라서 도달 가능한 모든 라이프타임 매개변수를 수집할 수 있으며, 이것이 바로 우리가 관심을 두는 것입니다.