C++에서 가상 함수 호출을 런타임에 해결하기 위한 함수 포인터 표 (virtual table).
구조
class Sensor {
public:
virtual int read() = 0;
virtual void init() { }
};
class TempSensor : public Sensor {
int read() override { return 25; }
};
TempSensor 객체
┌───────────────┐
│ vptr ─────────┼──► TempSensor의 vtable
│ (멤버 변수들) │ ┌───────────────────────┐
└───────────────┘ │ [0] read → 0x8001234 │
│ [1] init → 0x8001300 │
└───────────────────────┘
객체마다 vptr(4바이트)이 붙고, 클래스마다 vtable 하나가 생긴다.
호출 과정
Sensor *s = get_sensor();
s->read();
- ① 객체에서 vptr 을 읽는다 (메모리 접근 1)
- ② vtable에서 read의 주소를 읽는다 (메모리 접근 2)
- ③ 그 주소로 간접 점프
직접 호출보다 메모리 접근 2번 + 간접 분기가 더 든다.
C의 함수 포인터 구조체와 같다
// C로 같은 것을 만들면
typedef struct { int (*read)(void); void (*init)(void); } SensorOps;
typedef struct { const SensorOps *ops; int data; } Sensor;
sensor->ops->read(); // 정확히 vtable 과 동일한 동작
C++의 virtual은 이 패턴을 컴파일러가 자동화한 것이다. 이 대응을 알면 "가상 함수가 느리다"는 말의 의미가 구체적으로 이해된다.
비용 — 임베디드에서 따져야 할 것
- ① RAM : 객체마다 vptr 4바이트
- 작은 객체를 수천 개 만들면 무시할 수 없다
- ② 플래시: 클래스마다 vtable (가상 함수 개수 × 4바이트)
- ③ 속도 : 간접 호출. 인라인 불가. 분기 예측 실패 가능
- ④ 예측성: 호출 대상이 런타임에 정해져 WCET 분석이 어려워진다
④가 하드 실시간 시스템에서 문제가 된다. 어느 구현이 불릴지 모르면 최악 실행 시간을 계산할 수 없다.
언제 쓰고 언제 피하나
✅ 쓸 만한 경우
-
- 드라이버 추상화 (여러 센서를 같은 인터페이스로)
-
- 런타임에 구현을 바꿔야 할 때
-
- 호출 빈도가 낮은 곳
❌ 피할 경우
-
- ISR 안에서
-
- 초당 수만 번 호출되는 핫 패스
-
- 구현이 컴파일 시점에 정해져 있을 때
대안 — CRTP (컴파일 시점 다형성)
template <typename Derived>
class SensorBase {
public:
int read() { return static_cast<Derived*>(this)->read_impl(); }
};
class TempSensor : public SensorBase<TempSensor> {
public:
int read_impl() { return 25; }
};
vtable이 없고 인라인까지 된다. 가상 함수와 같은 코드 구조를 얻으면서 런타임 비용이 0이다.
한계 — 런타임에 타입을 바꿀 수 없다. 컴파일 시점에 정해진 다형성만 표현할 수 있다.
반드시 지킬 것 — 가상 소멸자
class Base { public: ~Base() { } }; // ✗
class Base { public: virtual ~Base() { } }; // ✓
Base *p = new Derived();
delete p; // 가상이 아니면 Derived의 소멸자가 안 불린다 → 자원 누수
다형적으로 삭제할 클래스에는 가상 소멸자가 필수다.
생성자·소멸자 안에서는 가상이 동작하지 않는다
class Base {
public:
Base() { init(); } // ✗ Base::init 이 불린다
virtual void init() { }
};
생성 중에는 vptr이 아직 Base를 가리킨다. 파생 클래스 부분이 초기화되지 않았으므로 언어가 이렇게 규정한 것이다. C++ 특유의 함정이며, 자바에서는 반대로 동작해 더 혼란스럽다.
확인 방법
arm-none-eabi-g++ -fdump-lang-class … # vtable 레이아웃 출력
arm-none-eabi-nm firmware.elf | grep vtable
_ZTV7Sensor 같은 심볼이 vtable이다. 맵 파일에서 크기를 확인할 수 있다.