31. 다중 상속의 객체 레이아웃과 this 포인터 조정(pointer adjustment)
난이도 최상내 리뷰 · C++
해설 · C++
해설 — 다중 상속의 객체 레이아웃과 this 포인터 조정(pointer adjustment)
난이도: 최상
예측 출력
1
0
16
C::fb
(A* 비교 = 1, B* 비교 = 0, B 서브오브젝트 오프셋 = 16, reinterpret_cast 비교 = 0, 그리고 C::fb)
전체 출력(줄 단위):
1
0
16
0
C::fb
핵심 개념
다중 상속 C : A, B 에서 C 객체는 메모리에 A 서브오브젝트 다음에 B 서브오브젝트가 이어 붙는 형태로
배치된다. 각 다형적 기반(A, B)은 자신의 vptr 를 가진다(Itanium C++ ABI, 64비트 기준).
C 객체 레이아웃(대략, 64비트):
offset 0 : [A vptr] (8)
offset 8 : A::a (4) + padding(4)
offset 16 : [B vptr] (8) <- B 서브오브젝트 시작
offset 24 : B::b (4)
offset 28 : C::c (4)
sizeof(C) = 32
단계별 내부 동작 (출력 근거)
A* pa = pc;— A 서브오브젝트는 오프셋 0 에 있으므로 주소 조정이 없다.pa == pc(as void*) →1.B* pb = pc;— B 서브오브젝트는 오프셋 16 에 있으므로, 컴파일러가 업캐스트 시this에 +16 을 더해 B 서브오브젝트를 가리키게 조정한다. 따라서pb != pc(as void*) →0.(char*)pb - (char*)pc= 16. B 서브오브젝트의 오프셋. (플랫폼/ABI에 따라 다를 수 있으나 이 환경은 16.)reinterpret_cast<B*>(pc)— reinterpret_cast 는 비트 재해석일 뿐 조정하지 않는다. 그래서pbBad는 C 시작 주소(=pc)를 그대로 B* 로 본다. 이는 잘못된 B 포인터다.pbBad == pb? pbBad 는 pc 주소, pb 는 pc+16 → 다르다 →0. (pbBad 로 B 멤버 접근 시 A 영역을 B로 오해해 UB.)pb->fb();— pb 는 B 서브오브젝트를 가리키고, B 의 vtable 슬롯에는C::fb로 가는 thunk가 들어있다. thunk 는this를 다시 -16 조정해 C* 로 만든 뒤C::fb를 호출한다. 출력C::fb.
왜 thunk 가 필요한가
C::fb 의 본문은 this 가 완전한 C 객체를 가리킨다고 가정한다(멤버 c 접근 등). 그런데 pb->fb() 는
B 서브오브젝트 주소(pc+16)로 가상 디스패치한다. 그래서 B 의 vtable 엔트리는 곧장 C::fb 가 아니라,
this -= 16 후 C::fb 로 점프하는 작은 adjustor thunk 를 가리킨다. 이 조정으로 오버라이더가 올바른
C* 를 받는다. A 쪽 vtable 의 fa 는 A 가 오프셋 0 이라 조정이 0 이라 thunk 가 없을 수 있다.
흔한 오해·함정
- static_cast vs reinterpret_cast: 관련 타입 간 업/다운 캐스트는 반드시
static_cast(또는 암시적)를 써야 포인터 조정이 일어난다.reinterpret_cast는 조정하지 않아 서브오브젝트 포인터가 어긋난다. - void* 왕복의 위험:
C*를void*로 지웠다가B*로 되살리면 조정 정보가 사라져 깨진다. 다형 객체를void*로 넘길 땐 가장 파생 타입 포인터를 저장하거나dynamic_cast<void*>(가장 파생 주소 획득)를 활용한다. - C-스타일 캐스트의 위험: 상황에 따라 static_cast 또는 reinterpret_cast 로 해석되어 예측이 어렵다.
- 단일 상속에서는 기반이 항상 오프셋 0 이라 조정이 없어 이런 문제가 드러나지 않는다 — 다중 상속에서만 두드러진다.
면접 포인트
- 다중 상속 객체의 메모리 배치와 각 기반의 vptr, 서브오브젝트 오프셋.
- 업캐스트 시
this조정이 필요한 이유와, static_cast(조정 O)/reinterpret_cast(조정 X)의 차이. - adjustor thunk 가 가상 디스패치에서 하는 역할(this 되돌림).