← 문제로

52. 생성자/소멸자 내부에서의 가상 함수 호출과 동적 타입의 변화

난이도 상
내 리뷰 · C++
해설 · C++

해설 — 생성자/소멸자 내부에서의 가상 함수 호출과 동적 타입의 변화

난이도: 상

요약

객체가 생성/소멸되는 중에는 그 시점의 동적 타입이 "현재 실행 중인 생성자/소멸자가 속한 클래스"로 고정된다. 그래서 Base 생성자 안에서 부른 OnInit()은 Actor를 오버라이드했더라도 Base::OnInit을 호출한다. 소멸자도 대칭으로, Base 소멸자 안의 OnShutdown()은 Base::OnShutdown을 부른다. 생성자와 소멸자에서의 가상 호출은 최종 오버라이더로 가지 않는다.

핵심 개념

  • 가상 디스패치는 객체의 vptr가 가리키는 vtable을 통해 이뤄진다.
  • 생성 순서: 기반(Base) → 파생(Actor). 각 생성자 진입 시 컴파일러가 vptr를 그 클래스의 vtable로 설정한다. 따라서 Base 생성자 실행 중에는 vptr가 Base의 vtable을 가리킨다.
  • 소멸 순서: 파생 → 기반(생성의 역순). 파생 소멸자 본문이 끝나고 기반 소멸자에 들어갈 때 vptr가 다시 Base vtable로 되돌려진다.
  • 이 규칙은 "아직 초기화되지 않은(또는 이미 파괴된) 파생 멤버를 가상 함수가 건드리는" 사고를 언어 차원에서 막기 위한 것이다.

단계별 내부 동작 (검증된 실제 출력)

--- construct ---
Base()  -> Base::OnInit        // Base 생성자 중: 동적 타입 = Base
Actor()                        // 이후 Actor 생성자 본문(buffer_ 할당 완료)
--- direct call ---
Actor::OnInit (buffer_=0x...)  // 완성된 객체에 대한 호출 → 최종 오버라이더
--- destruct ---
~Actor()                       // delete p: 파생 소멸자 본문 먼저(buffer_ 해제)
~Base() -> Base::OnShutdown    // 그 다음 기반 소멸자, vptr=Base → OnShutdown은 Base판

즉 delete p는 ~Actor()(본문·buffer_ 해제) → ~Base()(vptr가 Base로 복귀, OnShutdown은 Base판) 순으로 진행된다.

흔한 오해·함정

  • "가상 함수니까 생성자에서 불러도 파생 버전이 실행되겠지" — 아니다. 초기화 순서를 파생 생성자에 의존하는 OnInit을 Base 생성자에서 부르면, Actor::OnInit이 아니라 Base::OnInit이 불려 초기화 훅이 무력화된다(버그의 단골).
  • 순수 가상을 생성/소멸 중 호출하면: OnInit()이 = 0이고 Base 생성자에서 직접 부르면 정의가 없어 정의되지 않은 동작(대개 링크/런타임 "pure virtual call" 오류). 간접(다른 함수 경유) 호출도 동일 위험.
  • RAII 초기화 훅이 필요하면 생성자에서 가상 호출 대신 2단계 초기화(생성 후 별도 Init() 호출)나 팩토리에서 완성 후 훅 호출을 쓴다.

면접 포인트

  • 생성/소멸 중 동적 타입이 고정되는 이유와 vptr 재설정 타이밍.
  • "생성자에서 가상 함수 호출 금지" 관용구의 근거와 대안(2단계 초기화/팩토리/CRTP).
  • 순수 가상 함수를 생성/소멸 중 호출했을 때의 결과.