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(파생 동작)는 사라진다.
단계별 내부 동작
- (2) 참조
Monster& ref = b: 참조는 복사를 만들지 않는다.ref는b그 자체를 가리키고 vptr은 Boss의 것 →ref.hp()는 vtable을 타고Boss::hp→ 600. - (3) 포인터
Monster* ptr = &b: 마찬가지로 객체는 여전히 Boss.ptr->hp()→Boss::hp→ 600. - (4) 값 전달
printHp(Monster m): 인자b(Boss)로 파라미터m(Monster)을 복사 초기화 → Monster 복사 생성자 → Monster 부분만 복사, vptr=Monster.m.hp()는Monster::hp→ 100. (가상 함수라도 객체 자체가 Monster이므로 파생으로 갈 길이 없다.) - (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같은 데이터 손실도 동반한다(메모리만 줄어드는 게 아니라 상태가 사라짐).
면접 포인트
- 가상 디스패치는 vptr/vtable로 이뤄지고 vptr은 실제 생성된 타입이 정한다 — 참조/포인터는 보존, 값 복사는 기반으로 고정.
- 슬라이싱은 "파생을 기반 값으로 복사"할 때 발생(인자 값 전달,
vector<Base>, 기반 값 대입). 동작·데이터 모두 잘린다. - 다형성 보존:
const Base&/Base*/unique_ptr<Base>. 컨테이너엔 포인터/스마트포인터를 담는다.