← 문제로

5. 객체 슬라이싱(object slicing)과 가상 디스패치

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

해설 — 객체 슬라이싱(object slicing)과 가상 디스패치

난이도: 중상

요약

가상 함수는 객체의 vptr(가상 테이블 포인터)이 가리키는 vtable로 디스패치된다. vptr은 객체가 실제로 어떤 타입으로 생성/복사되었는지에 따라 정해진다. 참조/포인터로 다루면 원래 객체(Boss)의 vptr이 유지되어 Boss::hp가 불리지만, 값으로 기반 클래스에 복사하면 Boss의 파생 부분이 잘려 나가고(slicing) vptr이 Monster의 것으로 설정되어 Monster::hp가 불린다.

출력 예측

ref:  600     // (2) 참조 → 동적 타입 Boss → Boss::hp
ptr:  600     // (3) 포인터 → 동적 타입 Boss → Boss::hp
printHp: 100  // (4) 값 복사 → 슬라이싱 → 동적 타입 Monster → Monster::hp
vec:  100     // (5) vector<Monster> 에 슬라이스되어 저장 → Monster::hp

핵심 개념

  • vtable / vptr: 가상 함수를 가진 클래스의 각 객체는 숨은 vptr을 갖고, 이는 그 클래스의 vtable(가상 함수 주소 배열)을 가리킨다. x.hp()는 "객체의 vptr → vtable → hp 슬롯"으로 런타임에 호출 대상을 찾는다.
  • vptr은 생성자가 설정한다: 객체가 Boss로 생성되면 vptr은 Boss vtable을, Monster로 생성/복사되면 Monster vtable을 가리킨다.
  • 슬라이싱: 파생 객체를 기반 클래스 으로 복사하면, 컴파일러는 Monster의 복사 생성자(Monster(const Monster&))를 호출해 Monster 부분만 복사한다. 결과 객체는 순수한 Monster이고 vptr도 Monster의 것 — extra(파생 데이터)와 Boss::hp(파생 동작)는 사라진다.

단계별 내부 동작

  1. (2) 참조 Monster& ref = b: 참조는 복사를 만들지 않는다. refb 그 자체를 가리키고 vptr은 Boss의 것 → ref.hp()는 vtable을 타고 Boss::hp600.
  2. (3) 포인터 Monster* ptr = &b: 마찬가지로 객체는 여전히 Boss. ptr->hp()Boss::hp600.
  3. (4) 값 전달 printHp(Monster m): 인자 b(Boss)로 파라미터 m(Monster)을 복사 초기화 → Monster 복사 생성자 → Monster 부분만 복사, vptr=Monster. m.hp()Monster::hp100. (가상 함수라도 객체 자체가 Monster이므로 파생으로 갈 길이 없다.)
  4. (5) vector<Monster> + push_back(b): 원소 타입이 Monster라 각 슬롯은 Monster 크기. b는 그 슬롯에 슬라이스되어 복사된다 → v[0]은 순수 Monster → 100. (컨테이너가 기반 클래스 값이면 다형성은 보존되지 않는다.)

흔한 오해·함정

  • "가상 함수면 항상 파생이 불린다" → 객체가 무엇이냐가 먼저다. 값 복사로 기반 객체가 되면 파생은 호출될 수 없다.
  • 함수 인자를 Base로 값 전달하면 조용히 슬라이싱된다(경고도 잘 안 난다). 다형성을 원하면 const Base& 또는 Base*.
  • std::vector<Base>에 파생을 담으면 전부 슬라이스 → 다형 컨테이너는 std::vector<std::unique_ptr<Base>>(또는 Base*).
  • 슬라이싱은 extra 같은 데이터 손실도 동반한다(메모리만 줄어드는 게 아니라 상태가 사라짐).

면접 포인트

  1. 가상 디스패치는 vptr/vtable로 이뤄지고 vptr은 실제 생성된 타입이 정한다 — 참조/포인터는 보존, 값 복사는 기반으로 고정.
  2. 슬라이싱은 "파생을 기반 값으로 복사"할 때 발생(인자 값 전달, vector<Base>, 기반 값 대입). 동작·데이터 모두 잘린다.
  3. 다형성 보존: const Base&/Base*/unique_ptr<Base>. 컨테이너엔 포인터/스마트포인터를 담는다.