?Trait 바운드와 marker 타입

? (물음표) 트레잇 바운드는 트레잇이 선택 사항임을 표시하는 문법입니다.

trait X {}

struct A;
impl X for A {}
struct B;

fn foo<T>(_x: T)
where
    T: ?X {}

fn main() {
    foo(A);
    foo(B);
}

만약 T: ?X가 아닌, T: X 였다면, 오류가 발생했을 것입니다:

the trait bound `B: X` is not satisfied
the trait `X` is implemented for `A`

보통 ?Sized등의 marker 타입으로 ? 트레잇 바운드를 사용해보았을 겁니다:

fn foo<T>(x: &T)
where
    T: ?Sized + std::fmt::Debug,
{
    println!("{x:?}");
}

fn main() {
    let x = 42;
    foo(&x);
}

Sized는 컴파일 타임에 알려진 크기의 타입입니다. 예를 들어 [usize; 3]은 크기가 3이라는 알려진 타입입니다. 하지만 [usize]는 컴파일 타입에 길이가 얼만지 모릅니다.

보통 str 이나 [T] 등의 알려지지 않은 크기의 타입을 DST (Dynamically Sized Type)이라고 부릅니다.

이럴 때 ?Sized를 사용하여 컴파일 타임에 알려지지 않은 크기의 타입을 취급할 수 있습니다:

#![allow(unused)]
fn main() {
struct Foo<T>(T)
where
    T: ?Sized + std::fmt::Debug;

struct FooBar(Foo<[usize]>);
}

아까부터 marker 타입이 언급됐습니다.

요약

marker 타입은 정말 간단하게 말해서, 어떤 타입의 속성을 나타내는 구조체, 트레잇 등의 빈 타입입니다. 러스트의 타입은 고유한 속성에 따라, 다양한 방식으로 분류됩니다.

그러한 속성을 표시(명시)해주는 타입입니다. 위에서 언급한 Sizedmarker 타입이며, 구조체로 이루어진 PhantomData, PhantomPinned, 트레잇으로 이루어진 Copy, Send, Sync, Sized, Unpinmarker 타입입니다. (자세한 사항은 여기에서 확인할 수 있습니다.)

? 트레잇 바운드와 반대로, 부정 트레잇 바운드도 존재합니다: ! (느낌표) 이가 대표적으로 사용되는 예는 Rc<T> 입니다. Rc<T>에서는 SendSync을 구현하면 안 됩니다. (자칫하다간 데이터 레이스가 발생할 수 있기 때문) 때문에 Rc<T>의 구현을 보면

#![allow(unused)]
fn main() {
impl<T> !Send for Rc<T> where T: ?Sized { /* ... */ }
impl<T> !Sync for Rc<T> where T: ?Sized { /* ... */ }
}

처럼 구현된 것을 볼 수 있습니다. 반면 Arc의 구현을 보면 SendSync이 구현되어 있는 것을 확인할 수 있습니다:

#![allow(unused)]
fn main() {
impl<T> Send for Arc<T>
where
    T: Sync + Send + ?Sized { /* ... */ }

impl<T> Sync for Arc<T>
where
    T: Sync + Send + ?Sized { /* ... */ }
}