PhantomData

불안전한 코드와 작업을 하다 보면, 우리는 종종 타입이나 수명이 구조체와 논리적으로 연관되어 있지만, 실제로 그 필드의 일부분은 아닌 상황을 마주할 수 있습니다. 이런 상황은 보통 수명인 경우가 많은데요, 예를 들어 &'a [T]를 위한 Iter는 (거의) 다음과 같이 정의되어 있습니다:

#![allow(unused)]
fn main() {
struct Iter<'a, T: 'a> {
    ptr: *const T,
    end: *const T,
}
}

하지만 'a가 구조체의 본문에 쓰이지 않았기 때문에, 이 수명은 무제한이 됩니다. 이것이 역사적으로 초래해왔던 문제들 때문에, 무제한 수명과 이를 사용하는 타입은 구조체 선언에서 금지되었습니다. 따라서 우리는 어떻게든 이 타입들을 구조체 안에서 참조해야 합니다. 이것을 올바르게 하는 것은 올바른 변성과 해제 검사에 있어서 필수적입니다.

우리는 이것을 특별한 표시 타입인 PhantomData를 통해서 합니다. PhantomData는 공간을 차지하지 않지만, 컴파일러의 분석을 위해 주어진 타입의 필드를 흉내냅니다. 이 방식은 우리가 원하는 변성을 직접 타입 시스템에 말하는 것보다 더 오류에 견고하다고 평가되었습니다. 또한 이 방식은 자동 트레잇과 해제 검사에 필요한 정보 등의 유용한 것들을 컴파일러에게 제공합니다.

Iter는 논리적으로 여러 개의 &'a T를 포함하므로, 바로 이렇게 우리는 PhantomData에게 흉내내라고 할 것입니다:

#![allow(unused)]
fn main() {
use std::marker;

struct Iter<'a, T: 'a> {
    ptr: *const T,
    end: *const T,
    _marker: marker::PhantomData<&'a T>,
}
}

이렇게만 하면 됩니다. 수명은 제한될 것이고, 반복자는 'aT에 대해서 공변할 것입니다. 모든 게 그냥 마법처럼 동작할 겁니다.

제네릭 매개변수와 해제 검사

RFC 1238의 도움으로, 우리가 이런 코드를 쓴다면:

#![allow(unused)]
fn main() {
struct Vec<T> {
    data: *const T, // 변성을 위한 `*const`
    len: usize,
    cap: usize,
}

#[cfg(any())]
impl<T> Drop for Vec<T> { /* … */ }
}

impl<T> Drop for Vec<T>의 존재가 러스트로 하여금 Vec<T>T 타입의 값들을 소유한다고 (더 정확히는: Drop 구현에서 T 타입의 값들을 사용할 수 있다고) 간주하게 만들고, 따라서 러스트는 Vec<T>가 해제될 때 T 타입의 값들이 달랑거리는 것을 허용하지 않을 것입니다.

따라서 어떤 타입이 Drop impl을 가지고 있다면, 추가적으로 _owns_T: PhantomData<T> 필드를 선언하는 것은 불필요하고 아무것도 달성하지 않습니다, 해제 검사기가 볼 때에는요 (변성과 자동 트레잇들에서는 영향을 줍니다).

특수한 경우: 만약 PhantomData를 포함하는 타입이 그 자체로는 Drop 구현이 전혀 없지만, Drop 구현이 있는 다른 필드를 포함한다면, 여기에 명시된 해제 검사기/#[may_dangle] 사항들이 적용될 것입니다: 포함하는 타입이 범위 밖으로 벗어날 때, PhantomData<T> 필드는 T 타입이 해제되어도 괜찮도록 할 것입니다.


하지만 이런 상황은 때때로 과도하게 제한된 코드로 이어질 수 있습니다. 바로 그래서 표준 라이브러리는 불안정하고 unsafe한 속성을 써서 바로 이 문서에서 경고했던, 구식의 "수동" 해제 검사 방식으로 돌아가는 겁니다: #[may_dangle] 속성으로요.

예외: 표준 라이브러리의 특수한 경우와 불안정한 #[may_dangle]

이 섹션은 당신이 자신의 라이브러리 코드만을 작성한다면 넘어가도 됩니다. 하지만 표준 라이브러리가 실제 Vec 정의를 가지고 무엇을 하는지 궁금하다면, 건전성을 위해 여전히 _owns_T: PhantomData<T>가 필요하다는 것을 알아차릴 겁니다.

그 이유를 보려면 클릭하세요

다음의 예제를 생각해 봅시다:

fn main() {
    let mut v: Vec<&str> = Vec::new();
    let s: String = "Short-lived".into();
    v.push(&s);
    drop(s);
} // <- `v` 는 여기서 해제됩니다

정석적으로 impl<T> Drop for Vec<T> {를 정의하면, 위의 코드는 부정됩니다.

확실히 이런 경우에서는, 우리는 Vec<&'s str>, 즉 str's만큼 사는 레퍼런스들의 벡터를 가지고 있습니다. 하지만 let s: String에서는, 이것이 Vec보다 먼저 해제되고, impl<'s> Drop for Vec<&'s str> {의 정의가 사용됩니다.

이것이 의미하는 것은 만약 이런 Drop 구현이 사용된다면, 파기된, 혹은 달랑거리는 수명 's로 작업을 할 것이라는 점입니다. 하지만 이것은 함수 시그니처에 있는 모든 러스트 레퍼런스는 기본적으로 달랑거리지 않고 역참조해도 문제가 없다는 러스트 규칙에 반대됩니다.

따라서 러스트는 보수적으로 이 코드를 부정할 수밖에 없습니다.

그런데 실제 Vec의 경우에서, Drop 구현은 &'s str에 대해 신경쓰지 않는데, 이는 &'s str이 따로 Drop 구현이 없기 때문입니다: VecDrop 구현은 그저 버퍼를 해제하고 싶을 뿐이죠.

즉, Vec의 경우를 특별하게 구분해서, 또는 Vec의 특수한 성질을 이용해서 위의 코드가 컴파일되면 좋겠네요: Vec가지고 있는 &'s str들을 해제될 때 사용하지 않도록 약속할 수도 있겠어요.

이 약속은 #[may_dangle]로 표현될 수 있는 unsafe한 약속입니다:

unsafe impl<#[may_dangle] 's> Drop for Vec<&'s str> { /* … */ }

아니면 좀더 일반적으로 표현하자면:

unsafe impl<#[may_dangle] T> Drop for Vec<T> { /* … */ }

이것이 러스트의 해제 검사기가 해제되는 값의 타입 매개변수가 달랑거리지 않도록 하는, 보수적인 추측에서 탈출하도록 하는 unsafe한 방법입니다.

표준 라이브러리와 같이 이렇게 했다면, 우리는 T가 자체의 Drop 구현이 있는 경우를 조심해야 합니다. 이 경우에는, &'s strstruct PrintOnDrop<'s>(&'s str);로 바꾸는 것을 상상해 봅시다. 이 구조체는 자체의 Drop 구현에서 내부의 &'s str를 역참조하여 화면에 출력할 것입니다.

확실히 버퍼를 해제하기 전에 Drop for Vec<T> {는, 내부의 각 T들이 Drop 구현이 있을 때, 각 T들을 해제시켜야 합니다. PrintOnDrop<'s>의 경우에 Drop for Vec<PrintOnDrop<'s>>는 버퍼를 해제하기 전에 PrintOnDrop<'s>의 원소들을 해제시켜야 합니다.

따라서 우리가 's#[may_dangle]하다고 말할 때, 이것은 심하게 모호하게 말했던 것입니다. 우리는 대신 이렇게 말해야 할 것입니다: "'sDrop 구현에 구속받지 않는 한에서 달랑거릴 수도 있습니다". 혹은 더 일반적으로 이렇게요: "TDrop 구현에 구속받지 않는 한에서 달랑거릴 수도 있습니다". 이런 "예외의 예외"는 우리가 T를 소유할 때마다 발생하는 흔한 현상입니다. 이래서 러스트의 #[may_dangle]은 이런 예외 상황에 대해 알고, 따라서 구조체의 필드들에 제네릭 매개변수가 소유되는 때에 비활성화될 것입니다.

따라서 표준 라이브러리는 이렇게 결론을 내립니다:

#![allow(unused)]
fn main() {
#[cfg(any())]
// 우리는 `Vec`을 해제할 때 `T`를 사용하지 않도록 약속합니다…
unsafe impl<#[may_dangle] T> Drop for Vec<T> {
    fn drop(&mut self) {
        unsafe {
            if mem::needs_drop::<T>() {
                /* … 이 경우는 제외하고요, 어떤 경우냐면 … */
                ptr::drop_in_place::<[T]>(/* … */);
            }
            // …
            dealloc(/* … */)
            // …
        }
    }
}

struct Vec<T> {
    // … `Vec`이 `T`의 원소들을 소유하고,
    // 따라서 해제될 때 `T`의 원소들을 해제시킬 때 말이죠!
    _owns_T: core::marker::PhantomData<T>,

    ptr: *const T, // 변성을 위한 `*const`입니다 (하지만 이것이 *그 자체로* `T`의 소유권을 나타내는 것은 아닙니다)
    len: usize,
    cap: usize,
}
}

할당된 메모리를 소유하는 생 포인터는 너무나 흔한 패턴이여서, 표준 라이브러리는 이것을 위한 도구인 Unique<T>를 만들었는데, 이것은:

  • 변성을 위해 *const T를 안에 포함합니다
  • PhantomData<T>를 포함합니다
  • 마치 T가 포함된 것처럼 Send/Sync를 자동으로 구현합니다
  • 널 포인터 최적화를 위해 포인터를 NonZero로 표시합니다

PhantomData 패턴의 표

여기 PhantomData가 사용될 수 있는 모든 경우의 놀라운 표가 있습니다:

흉내내는 타입'a의 변성T의 변성Send/Sync
(or lack thereof)
해제 구현에서 'aT의 달랑거림
(: #[may_dangle] Drop)
PhantomData<T>-전달불가 ("T를 소유")
PhantomData<&'a T>T : Sync
이면
Send + Sync
가능
PhantomData<&'a mut T>전달가능
PhantomData<*const T>-!Send + !Sync가능
PhantomData<*mut T>-!Send + !Sync가능
PhantomData<fn(T)>-Send + Sync가능
PhantomData<fn() -> T>-Send + Sync가능
PhantomData<fn(T) -> T>-Send + Sync가능
PhantomData<Cell<&'a ()>>-Send + !Sync가능
  • 주의: 자동으로 구현되는 Unpin 트레잇을 구현하지 않으려면 따로 선언된 PhantomPinned 타입을 사용해야 합니다.