7. 가상 상속(virtual inheritance)과 다이아몬드 상속
난이도 상내 리뷰 · C++
해설 · C++
해설 — 가상 상속(virtual inheritance)과 다이아몬드 상속
난이도: 상
요약
다이아몬드 상속(D ← L,R ← Base)에서 가상 상속(: virtual Base)을 쓰면 D 안에 Base 서브객체가 단 하나만 존재한다. 그래서 d.x 접근이 모호하지 않고, 그 하나의 가상 기반은 가장 파생된 클래스 D 의 생성자가 책임지고 한 번 초기화한다. 비가상 상속이면 Base 가 두 개(L 경로, R 경로) 생겨 d.x 가 모호해지고 Base 도 두 번 생성된다. 가상 상속은 이를 위해 객체에 가상 기반 오프셋(vptr/vbase 포인터) 을 추가해 레이아웃·접근 비용이 커진다.
출력 예측
Base()
L()
R()
D()
x=5
who: Base
핵심 개념
- 가상 기반(virtual base)은 한 번만:
L : virtual Base,R : virtual Base이면 D 에서 L·R 이 공유하는 하나의 Base 만 생긴다(공유 기반). - 가상 기반의 생성은 most-derived 가 담당: 가상 기반은 중간 클래스(L, R)가 아니라 가장 파생된 객체(D) 의 생성자가 직접 초기화한다. 그래서 Base 생성자는 딱 한 번, 그리고 가장 먼저 불린다.
- 생성 순서: 가상 기반(Base) → 비가상 기반들을 선언 순서로(L, 그다음 R) → 파생(D). 따라서
Base() → L() → R() → D(). - 모호성 해소: Base 가 하나뿐이라
d.x는 유일 → 모호하지 않다.
단계별 내부 동작
D d;→ D 생성 시작: 컴파일러가 가상 기반 Base 를 먼저 구성 →Base().- 이어서 비가상 직접 기반을 선언 순서(L, R) 로 구성하되, 이들의 생성자는 가상 기반을 다시 생성하지 않는다(이미 D 가 만들었음) →
L(),R(). - 마지막으로
D(). d.x = 5→ 공유 Base 의 x 에 접근(유일하므로 명확).who()는 가상 함수, 동적 타입 D 지만 D 가 오버라이드 안 했으니Base::who→who: Base.
비가상 상속이라면 (대조)
: virtual을 빼면 D 안에 Base 가 둘(L::Base,R::Base) 생긴다.Base()가 두 번 출력되고, 생성 순서는Base()(L용) L() Base()(R용) R() D().d.x는 어느 Base 의 x 인지 모호(ambiguous) → 컴파일 에러.d.L::x/d.R::x로 한정하거나 캐스팅해야 함.
객체 레이아웃 / sizeof
- 가상 상속은 "공유 기반이 객체 안 어디에 있는지"를 런타임에 찾아야 하므로, 각 서브객체에 가상 기반 오프셋 정보(vbase 포인터, 흔히 vptr 에 통합) 를 둔다. 그래서 가상 상속 객체는 추가 포인터만큼 커지고, 가상 기반 멤버 접근이 간접(오프셋 조회) 이 된다.
- 측정 예(구현 의존, 본 toolchain x86-64):
sizeof(Base)=16,sizeof(L)=24,sizeof(D)=32. 정확한 수치는 ABI/컴파일러마다 다르다 — 요점은 "가상 상속은 공짜가 아니다(포인터 + 간접 접근)"이다.
흔한 오해·함정
- "가상 상속이면 항상 더 작다/크다" — 크기는 구현 의존. 핵심은 Base 서브객체 개수(1 vs 2) 와 생성 책임(most-derived).
- 가상 기반 클래스에 생성자 인자가 필요하면 D 가 직접 그 인자를 넘겨야 한다(L/R 의 Base 초기화 인자는 무시된다).
- 다중상속·가상상속은 캐스팅(특히
static_castvsdynamic_cast) 시 포인터 보정이 필요할 수 있다 — 가상 기반으로의static_cast제약을 안다.
면접 포인트
- 다이아몬드에서 가상 상속의 효과: 공유 기반 1개, most-derived 가 가상 기반 생성, 생성 순서(가상 기반 먼저).
- 비가상이면 기반 중복 → 멤버 접근 모호 + 기반 중복 생성.
- 가상 상속의 비용: vbase 오프셋(포인터) 추가 + 간접 멤버 접근 — 게임 코드에서 남발하지 않는 이유.