49. 공변 반환 타입과 반환 포인터 조정(covariant return / pointer adjustment)
난이도 중내 리뷰 · C++
해설 · C++
해설 — 공변 반환 타입과 반환 포인터 조정(covariant return / pointer adjustment)
난이도: 중상
요약
가상 함수를 재정의할 때 반환 타입을 "더 파생된 포인터/참조"로 좁힐 수 있다(공변 반환). 기반 타입(Factory*)으로 호출하면 정적 반환형은 RetBase* 지만 실제 구현은 RetDerived* 를 돌려준다. 반환되는 RetDerived 안에서 RetBase 서브오브젝트가 오프셋 0이 아니면, 컴파일러는 vtable 썽크로 반환 포인터를 그 오프셋만큼 더해 유효한 RetBase* 로 조정한다. 예측 출력: 10 / 16 / true / 20.
핵심 개념
- 공변 반환(covariant return):
Base* f()를 재정의하며Derived* f()로 반환형을 좁히는 것이 허용된다(Derived*가Base*로 암시적 변환 가능하고, 접근·cv 규칙을 만족할 때). - 포인터 조정(pointer adjustment): 다중 상속에서 base 서브오브젝트는 파생 객체 내 서로 다른 오프셋에 놓인다.
Derived*↔Base*변환은 단순 비트 복사가 아니라 오프셋 가감이다. - 썽크(thunk): 가상 디스패치가
RetBase*를 기대하는데 오버라이더가RetDerived*를 반환하므로, vtable 항목이 조정 코드를 거쳐 반환값을RetBase*로 바꾼다.
단계별 내부 동작
레이아웃 (Itanium ABI)
struct RetDerived : Pad, RetBase. Pad 는 가상 소멸자/함수를 가져 다형적이므로 primary base 로 선택되어 오프셋 0에 놓이고 vptr 을 공유한다.
Pad: vptr(8) +p(8) = 16바이트, 오프셋 0.RetBase: 자체 vptr(8) +x(4) + 패딩 = 16바이트, 오프셋 16.y는 그 뒤.
그래서 RetDerived 시작 주소에서 RetBase 서브오브젝트까지의 오프셋 = 16.
호출 경로
rd = cf.make(): 정적 반환형이RetDerived*.rd는RetDerived객체(=storage) 시작 주소.rb = f->make():f는Factory*. 가상 디스패치로ConcreteFactory::make가 불리고RetDerived*를 만들지만, 이 가상 슬롯의 정적 반환형은RetBase*. 썽크가RetDerived*에 +16 을 더해RetBase서브오브젝트를 가리키는RetBase*로 조정한 값을 반환한다.
각 줄
- L1
rb->x=10.rb는 조정된 뒤 정확히RetBase서브오브젝트를 가리키므로x접근이 올바르다. - L2
(char*)rb - (char*)rd=16. 조정 오프셋(RetBase 서브오브젝트 오프셋)이 그대로 드러난다. - L3
static_cast<RetDerived*>(rb) == rd=true.RetBase*→RetDerived*다운캐스트는 -16 조정을 수행해 원래RetDerived시작 주소로 되돌린다. 두 포인터가 같아진다. - L4
rd->y=20.
흔한 오해·함정
- "포인터 값은 그대로일 것" 이라는 오해: 다중 상속에서 base/derived 포인터 변환은 값이 바뀐다.
reinterpret_cast로 강제하면 조정이 생략되어 잘못된 base 포인터가 되고 UB. 반드시static_cast(또는 암시적 변환)를 써야 컴파일러가 오프셋을 가감한다. - 공변 반환은 포인터/참조에만: 값 반환(
RetBaseby value)은 공변 불가(슬라이싱 문제). - 오프셋 0이면 조정 없음: 앞 실험처럼 다형적 base 하나만 있으면 그것이 primary base(오프셋 0)라 조정이 0이라 "그냥 되는 것처럼" 보인다. 다중 상속에서 오프셋이 생겨야 썽크가 드러난다.
- 가상 상속이 섞이면 조정이 vtable 의 vbase 오프셋을 통한 더 복잡한 런타임 계산이 된다.
면접 포인트
- 공변 반환이 언제 허용되며 값 반환에 안 되는 이유(슬라이싱).
- 다중 상속에서
Derived*↔Base*변환이 오프셋 가감이라는 점,static_castvsreinterpret_cast차이. - primary base 선택 규칙(다형적 base 를 오프셋 0에)과 그로 인해 조정이 보이거나 숨는 이유.
- 관련: problem31 은 멤버 호출 시 this 포인터 조정, 본 문제는 반환값 포인터 조정(썽크) — 조정 대상이 다르다.