C++ 가상 함수 디스패치와 vtable 구조
vtable이란
섹션 제목: “vtable이란”virtual 키워드가 붙은 함수를 포함하는 클래스는 컴파일러가 **vtable(가상 함수 테이블)**을 생성합니다. vtable은 함수 포인터 배열이며, 각 가상 함수의 실제 구현 주소를 담습니다. 객체마다 vtable을 가리키는 vptr이 하나씩 숨겨진 멤버로 추가됩니다.
class Base {public: virtual void draw() {} virtual void update() {} int x;};
class Derived : public Base {public: void draw() override {} // vtable 슬롯 교체};메모리 레이아웃:
Base 객체: [ vptr → Base_vtable | x ]Derived 객체: [ vptr → Derived_vtable | x ]
Base_vtable: [ &Base::draw | &Base::update ]Derived_vtable: [ &Derived::draw | &Base::update ] ← draw만 교체vptr 초기화 시점
섹션 제목: “vptr 초기화 시점”생성자 호출 순서에 따라 vptr이 단계적으로 설정됩니다.
struct A { A() { /* vptr → A::vtable */ } virtual void f() {}};
struct B : A { B() { /* vptr → B::vtable 으로 갱신 */ } void f() override {}};생성자 안에서 가상 함수를 호출하면 아직 파생 클래스의 vptr이 설정되지 않아 기대하는 오버라이드가 실행되지 않습니다. 소멸자도 동일합니다.
다중 상속과 vtable
섹션 제목: “다중 상속과 vtable”다중 상속 시 객체 내에 vptr이 여러 개 존재합니다.
class A { virtual void f(); };class B { virtual void g(); };class C : public A, public B { void f() override; void g() override;};// C 레이아웃: [ vptr_A | A 필드 | vptr_B | B 필드 | C 필드 ]B* 포인터로 캐스팅하면 내부 주소 오프셋이 적용되므로 reinterpret_cast는 위험하고 static_cast나 dynamic_cast를 사용해야 합니다.
성능 특성
섹션 제목: “성능 특성”| 항목 | 내용 |
|---|---|
| vptr 역참조 | 포인터 1회 추가 역참조 |
| 간접 분기 | CPU 분기 예측기 미스 가능 |
| 인라인 불가 | 컴파일 타임에 호출 대상 미확정 |
| devirtualization | 컴파일러가 호출 대상을 정적으로 파악하면 직접 호출로 최적화 |
Derived d;d.draw(); // 타입 확정 → devirtualization 가능
Base* p = &d;p->draw(); // 타입 불확실 → vtable 조회final로 devirtualization 힌트 제공
섹션 제목: “final로 devirtualization 힌트 제공”class Derived final : public Base { void draw() override final {}};final은 파생 클래스가 없음을 컴파일러에 알려 가상 호출을 직접 호출로 최적화할 기회를 줍니다.
순수 가상 함수
섹션 제목: “순수 가상 함수”class Shape {public: virtual ~Shape() = default; virtual double area() const = 0; // 순수 가상};순수 가상 함수의 vtable 슬롯은 __cxa_pure_virtual 같은 에러 핸들러를 가리킵니다. 직접 호출 시 프로그램이 종료됩니다.
- vtable은 클래스당 하나, vptr은 객체당 하나(다중 상속 시 여러 개)
- 생성자/소멸자 내 가상 함수 호출은 파생 클래스 구현이 실행되지 않으므로 피할 것
final지정으로 devirtualization 최적화 유도 가능- 가상 함수 오버헤드는 대부분 무시할 수준이나, 캐시 미스와 분기 예측 실패가 핫 루프에서는 병목이 될 수 있음