Rust dyn Trait의 fat pointer와 vtable을 C++ 다형성과 메모리 배치로 해부
- Rust의 정적 디스패치는
T: Draw같은 제네릭 호출마다draw_shape::<Circle>,draw_shape::<Square>처럼 타입별 함수를 생성하는 단형화 방식이며, 타입을 컴파일 시점에 알아야 함 - 런타임 다형성이 필요할 때 Rust는
dyn Trait를 사용하며, 글은 C++ 가상 함수의 객체 내 vtable 포인터와 대비해 trait object의 메모리 동작을 설명함 - C++ 템플릿은
.draw()메서드가 있는 타입을 암묵적으로 받지만, Rust는impl Draw for Square처럼 trait 계약을 명시함 - 필드가 없는
Circle,Square는 Rust에서 제로 크기 타입(ZST) 이므로std::mem::size_of결과가 0이고, 별도 메모리를 할당하지 않음 - 토론에서는 ZST의 동일 위치 비교에
std::ptr::eq를 쓸 수 있으며, 빌림 검사기는 포인터 동등성 의미를 정하는 도구가 아니라 참조의 수명과 별칭 규칙을 검사한다는 지적이 나옴
Hacker News opinions
ZST가 같은 대상인지 C++처럼 포인터 비교로 확인할 수 있는데, 왜 빌림 검사기가 그 질문을 없애 준다는 건지 설명이 부족해 보임. 검사기가 그 정보를 원하면 어떻게 얻는지도 모르겠음.
빌림 검사기가 같은 객체인지 안다는 말보다는, 같은 객체에 대한 가변 참조와 불변 참조의 불법 충돌이 없음을 증명한다는 쪽이 맞아 보임. 이 성질이 필요하면 1바이트 필드를 넣고 포인터 동등성을 쓰면 될 듯함.
C++의 포인터가 가리키는 객체에는 항상 정체성이 있지만 Rust 값은 0이 아닌 크기일 때만 정체성이 있다는 차이로 이해함. 쓰지 않는 비용을 내지 않는 원칙의 사례임.
포인터가 같은 위치를 가리키는지 비교하려면 std::ptr::eq를 쓰면 됨. Rust가 참조 동등성보다 값 동등성을 기본으로 할 뿐이고, ZST에만 해당하는 얘기도 아님. 빌림 검사기에 저장돼 있다는 글의 표현은 좀 이상함.
실제 알고리즘에서 ZST 포인터 둘을 비교해야 할 경우를 몇 분 생각해 봤는데 떠오르지 않았음. 그런 사례가 있나?
글 좋았음. 다음 글에서는 vtable 구조 자체도 역공학해 보면 좋겠음. 메서드 구현체 포인터 목록 같은 구조일 것이라 추측함.
메서드 포인터만 있는 건 아니고 정렬 같은 정보도 들어감. Arc 다운캐스팅 관련 글에서 이 부분을 다뤘음.
글에서 쓴 Object Safety는 요즘 Rust가 dyn compatibility라고 부르는 개념임. trait이 dyn 호환이면 dyn Trait로 쓸 수 있다는 뜻이 더 잘 드러나고, Rust 레퍼런스가 규칙도 더 자세히 설명함.
C++는 가상 호출이 포인터를 거치고 반환형도 항상 포인터라서 이 문제가 없다는 설명은 틀림. C++도 값을 반환할 수 있고, 구체 타입 크기를 알아야 저장할 수 있다는 같은 문제가 생김. Rust도 Self 대신 Box<dyn Trait>를 돌려주는 DynClone 같은 trait으로 풀 수 있음.