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

드롭 검사

일반적으로 지역 변수가 사용될 때마다 해당 지역 변수의 타입이 적격이어야 합니다. 여기에는 지역 변수의 where 바운드를 증명하는 것이 포함되며, 지역 변수가 사용하는 모든 영역(region)이 살아 있어야 한다는 조건도 포함됩니다.

이에 대한 유일한 예외는 값이 스코프를 벗어날 때 암묵적으로 드롭되는 경우입니다. 이 경우 값이 반드시 살아 있어야 하는 것은 아닙니다:

fn main() {
    let x = vec![];
    {
        let y = String::from("I am temporary");
        x.push(&y);
    }
    // `x` goes out of scope here, after the reference to `y`
    // is invalidated. This means that while dropping `x` its type
    // is not well-formed as it contain regions which are not live.
}

이는 값을 드롭할 때 죽은 영역에 접근하지 않는 경우에만 건전합니다. 값의 타입이 drop-live 상태여야 한다고 요구하여 이를 검사합니다. 이에 대한 요구 사항은 fn dropck_outlives에서 계산됩니다.

이 절의 나머지 부분에서는 영역 매개변수가 살아 있어야 하는 타입으로 다음 타입 정의를 사용합니다:

#![allow(unused)]
fn main() {
struct PrintOnDrop<'a>(&'a str);
impl<'a> Drop for PrintOnDrop<'_> {
    fn drop(&mut self) {
        println!("{}", self.0);
    }
}
}

값이 드롭되는 방식

근본적으로 T 타입의 값은 해당 값의 “드롭 글루(drop glue)“를 실행하여 드롭됩니다. 드롭 글루는 컴파일러가 생성하며, 먼저 <T as Drop>::drop을 호출한 다음 재귀적으로 소유된 모든 값의 드롭 글루를 재귀적으로 호출합니다.

  • T에 명시적인 Drop impl이 있다면 <T as Drop>::drop을 호출하십시오.
  • T가 Drop을 구현하는지 여부와 관계없이 T가 소유한 모든 값으로 재귀합니다:
    • 참조, 원시 포인터, 함수 포인터, 함수 항목, 트레이트 객체1, 스칼라는 아무것도 소유하지 않습니다.
    • 튜플, 슬라이스, 배열은 자신의 요소를 소유한 것으로 간주합니다. 길이가 0인 배열은 요소 타입의 값을 전혀 소유하지 않습니다.
    • ADT의 (모든 배리언트의) 모든 필드는 소유된 것으로 간주됩니다. 열거형의 경우 모든 배리언트를 고려합니다. 여기서 예외는 ManuallyDrop<U>로, 이는 U를 소유하지 않는 것으로 간주됩니다. PhantomData<U> 역시 아무것도 소유하지 않습니다. 클로저와 제너레이터는 캡처한 업바(upvar)를 소유합니다.

타입에 드롭 글루가 있는지 여부는 fn Ty::needs_drop이 반환합니다.

지역 변수의 부분 드롭

Drop을 직접 구현하지 않는 타입의 경우, 나머지를 드롭하기 전에 값의 일부를 먼저 이동할 수도 있습니다. 이 경우 아직 이동되지 않은 값의 드롭 글루만 호출됩니다. 예를 들면 다음과 같습니다.

fn main() {
    let mut x = (PrintOnDrop("third"), PrintOnDrop("first"));
    drop(x.1);
    println!("second")
}

MIR 빌드 중에는 지역 변수의 타입이 드롭을 필요로 하는 한, 지역 변수가 스코프를 벗어날 때마다 드롭될 수 있다고 가정합니다. 변수의 정확한 드롭 글루는 borrowck 이후에 ElaborateDrops 패스에서 계산됩니다. 따라서 지역 변수의 일부가 이전에 드롭되었더라도 dropck는 여전히 이 값이 살아 있을 것을 요구합니다. 이는 지역 변수를 완전히 이동한 경우에도 마찬가지입니다.

fn main() {
    let mut x;
    {
        let temp = String::from("I am temporary");
        x = PrintOnDrop(&temp);
        drop(x);
    }
} //~ ERROR `temp` does not live long enough.

borrowck 이전에 어느 정도의 드롭 정교화(drop elaboration)를 추가하여 이 예제가 컴파일되도록 하는 것이 가능할 것입니다. 드롭 정교화를 const 검사 이전으로 옮기는 불안정 기능이 있습니다: #73255. 관련 MCP가 있기는 하지만, borrowck 이전에 일부 드롭 정교화를 수행하기 위한 기능 게이트는 존재하지 않습니다.

dropck_outlives

우리가 수행하는 “활성(liveness)” 계산은 서로 다른 두 가지입니다.

  • 값 v가 이후에 “사용“될 수 있으면 해당 값은 위치 L에서 use-live입니다. 여기서 사용이란 기본적으로 드롭이 아닌 모든 것을 말합니다.
  • 값 v가 이후에 드롭될 수 있으면 해당 값은 위치 L에서 drop-live입니다.

어떤 값이 use-live이면 그 타입 전체가 L에서 유효해야 합니다. 어떤 값이 drop-live이면 dropck가 요구하는 모든 리전이 L에서 유효해야 합니다. MIR에서 드롭되는 값은 플레이스(place)입니다.

dropck_outlives가 어떤 타입에 대해 계산하는 제약 조건은 그 타입에 대해 생성되는 드롭 글루(drop glue)와 거의 일치합니다. 드롭 글루와 달리 dropck_outlives는 값 자체가 아니라 소유된 값의 타입을 고려합니다. 타입이 T인 값에 대해서는 다음과 같습니다.

  • T에 명시적인 Drop이 있으면 모든 제네릭 인자가 살아 있을 것을 요구합니다. 단, #[may_dangle]로 표시된 인자는 완전히 무시합니다.
  • T에 명시적인 Drop이 있는지와 관계없이 T가 소유한 모든 타입으로 재귀합니다.
    • 참조, 원시 포인터, 함수 포인터, 함수 항목, 트레이트 객체1, 스칼라는 아무것도 소유하지 않습니다.
    • 튜플, 슬라이스, 배열은 자신의 원소 타입을 소유한 것으로 간주합니다. 현재 배열에 대해서는 길이가 0인지 검사하지 않습니다.
    • ADT의 (모든 variant의) 모든 필드는 소유된 것으로 간주합니다. 여기서 예외는 ManuallyDrop<U>로, 이 타입은 U를 소유하지 않는 것으로 간주합니다. PhantomData<U>는 U를 소유하는 것으로 간주합니다.
    • 클로저와 제너레이터는 자신이 캡처한 upvar를 소유합니다.

굵게 표시된 부분은 Ty::needs_drop은 무시하지만 dropck_outlives는 소유된 것으로 간주하는 경우입니다. 우리는 해당 타입을 담고 있는 로컬에 대해 Ty::needs_drop이 true를 반환한 경우에만 dropck_outlives에 의존합니다. 따라서 어떤 타입이 더 큰 로컬 안에 포함되어 있는지에 따라 활성 요구 사항이 달라질 수 있습니다. 이는 일관성이 없으므로 수정되어야 합니다. 배열에 대한 예시와 PhantomData에 대한 예시를 참고하십시오.2

이러한 불일치를 해결할 수 있는 한 가지 방법은 MIR 빌드를 더 비관적으로 만드는 것으로, 아마도 Ty::needs_drop을 더 약하게 만드는 방식이 될 것입니다. 또는 dropck_outlives를 더 정밀하게 바꾸어 살아 있어야 하는 리전의 수를 줄이는 방법도 있습니다.


  1. 트레이트 객체에는 vtable이 제공하는 drop_in_place를 직접 사용하는 내장 Drop 구현이 있다고 볼 수 있습니다. 이 Drop 구현은 모든 제네릭 매개변수가 살아 있을 것을 요구합니다. ↩ ↩2

  2. 이것은 #110288과 RFC 3417의 핵심 가정입니다. ↩