콘텐츠로 이동

C++ 가상 함수 디스패치와 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이 단계적으로 설정됩니다.

struct A {
A() { /* vptr → A::vtable */ }
virtual void f() {}
};
struct B : A {
B() { /* vptr → B::vtable 으로 갱신 */ }
void f() override {}
};

생성자 안에서 가상 함수를 호출하면 아직 파생 클래스의 vptr이 설정되지 않아 기대하는 오버라이드가 실행되지 않습니다. 소멸자도 동일합니다.

다중 상속 시 객체 내에 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_castdynamic_cast를 사용해야 합니다.

항목내용
vptr 역참조포인터 1회 추가 역참조
간접 분기CPU 분기 예측기 미스 가능
인라인 불가컴파일 타임에 호출 대상 미확정
devirtualization컴파일러가 호출 대상을 정적으로 파악하면 직접 호출로 최적화
Derived d;
d.draw(); // 타입 확정 → devirtualization 가능
Base* p = &d;
p->draw(); // 타입 불확실 → vtable 조회
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 최적화 유도 가능
  • 가상 함수 오버헤드는 대부분 무시할 수준이나, 캐시 미스와 분기 예측 실패가 핫 루프에서는 병목이 될 수 있음