← 문제로

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 는 유일 → 모호하지 않다.

단계별 내부 동작

  1. D d; → D 생성 시작: 컴파일러가 가상 기반 Base 를 먼저 구성 → Base().
  2. 이어서 비가상 직접 기반을 선언 순서(L, R) 로 구성하되, 이들의 생성자는 가상 기반을 다시 생성하지 않는다(이미 D 가 만들었음) → L(), R().
  3. 마지막으로 D().
  4. d.x = 5 → 공유 Base 의 x 에 접근(유일하므로 명확). who() 는 가상 함수, 동적 타입 D 지만 D 가 오버라이드 안 했으니 Base::whowho: 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_cast vs dynamic_cast) 시 포인터 보정이 필요할 수 있다 — 가상 기반으로의 static_cast 제약을 안다.

면접 포인트

  1. 다이아몬드에서 가상 상속의 효과: 공유 기반 1개, most-derived 가 가상 기반 생성, 생성 순서(가상 기반 먼저).
  2. 비가상이면 기반 중복 → 멤버 접근 모호 + 기반 중복 생성.
  3. 가상 상속의 비용: vbase 오프셋(포인터) 추가 + 간접 멤버 접근 — 게임 코드에서 남발하지 않는 이유.