Post

C++ 가상 함수와 vptr: 런타임 다형성의 핵심 메커니즘

📌 학습 목표

  • vptr(virtual pointer)의 역할과 동작 원리 완벽 이해
  • 가상 함수 호출 시 vptr을 통한 함수 디스패치 과정 분석
  • vptr로 인한 메모리 오버헤드와 성능 영향 평가
  • 가상 함수의 적절한 사용 시점과 최적화 방법 학습

📝 개념 정리

vptr (Virtual Pointer)이란?

핵심 개념:

  • 클래스에 virtual 함수가 있으면 객체에 vptr(virtual pointer) 생성
  • vptr은 해당 클래스의 vtable을 가리키는 포인터
  • 런타임에 올바른 함수를 찾아 호출하는 다형성의 핵심 메커니즘
  • 객체의 첫 번째 멤버로 저장됨 (일반적으로)

기본 동작 원리:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Animal {
public:
    virtual void speak() { cout << "Animal sound" << endl; }
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void speak() override { cout << "Woof!" << endl; }
};

// 메모리 레이아웃:
// Animal 객체: [vptr] -> Animal vtable
// Dog 객체:    [vptr] -> Dog vtable

💻 vptr 동작 메커니즘 상세 분석

1. 객체 생성과 vptr 초기화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
#include <memory>

class Base {
public:
    Base() { 
        cout << "Base 생성자: vptr이 Base vtable로 초기화됨" << endl; 
    }
    virtual void identify() { cout << "I am Base" << endl; }
    virtual ~Base() { 
        cout << "Base 소멸자" << endl; 
    }
};

class Derived : public Base {
public:
    Derived() { 
        cout << "Derived 생성자: vptr이 Derived vtable로 재설정됨" << endl; 
    }
    void identify() override { cout << "I am Derived" << endl; }
    ~Derived() { 
        cout << "Derived 소멸자" << endl; 
    }
};

void demonstrateVptrInitialization() {
    cout << "=== 객체 생성 과정에서의 vptr 변화 ===" << endl;
    
    // Derived 객체 생성 시:
    // 1. Base 생성자 호출 -> vptr이 Base vtable 가리킴
    // 2. Derived 생성자 호출 -> vptr이 Derived vtable로 변경
    Derived obj;
    
    cout << "\n=== 다형적 호출 ===" << endl;
    Base* ptr = &obj;
    ptr->identify(); // "I am Derived" 출력 (vptr이 Derived vtable을 가리키므로)
}

2. 함수 호출 시 vptr을 통한 디스패치

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void analyzeVirtualCall() {
    cout << "=== 가상 함수 호출 분석 ===" << endl;
    
    Base* ptr = new Derived();
    
    // ptr->identify() 호출 시 내부 동작:
    // 1. ptr이 가리키는 객체의 주소에서 vptr 읽기
    // 2. vptr이 가리키는 vtable에서 identify 함수의 인덱스 위치 찾기
    // 3. 해당 위치의 함수 포인터 읽기
    // 4. 함수 포인터가 가리키는 주소로 점프
    
    ptr->identify(); // Derived::identify 호출
    
    delete ptr;
}

3. vptr 메모리 레이아웃 확인

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <cstdint>

class MemoryAnalysis {
public:
    int data1;
    virtual void func1() {}
    int data2;
    virtual void func2() {}
    
    void analyzeMemoryLayout() {
        cout << "=== 객체 메모리 레이아웃 분석 ===" << endl;
        cout << "객체 주소: " << this << endl;
        cout << "객체 크기: " << sizeof(*this) << " bytes" << endl;
        
        // vptr은 보통 객체의 첫 번째 위치에 저장됨
        uintptr_t* vptr = *reinterpret_cast<uintptr_t**>(this);
        cout << "vptr 주소: " << vptr << endl;
        
        // 멤버 변수들의 위치
        cout << "data1 주소: " << &data1 << endl;
        cout << "data2 주소: " << &data2 << endl;
        
        // vptr과 멤버 변수들 간의 오프셋 계산
        cout << "data1 오프셋: " << 
            reinterpret_cast<char*>(&data1) - reinterpret_cast<char*>(this) << endl;
        cout << "data2 오프셋: " << 
            reinterpret_cast<char*>(&data2) - reinterpret_cast<char*>(this) << endl;
    }
};

class NoVirtual {
public:
    int data1;
    int data2;
    void func() {} // 비가상 함수
};

void compareMemoryUsage() {
    cout << "=== 메모리 사용량 비교 ===" << endl;
    cout << "가상 함수 없는 클래스: " << sizeof(NoVirtual) << " bytes" << endl;
    cout << "가상 함수 있는 클래스: " << sizeof(MemoryAnalysis) << " bytes" << endl;
    cout << "vptr 오버헤드: " << sizeof(MemoryAnalysis) - sizeof(NoVirtual) << " bytes" << endl;
}

⚡ 성능 영향과 최적화

1. 가상 함수 호출 비용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <chrono>
#include <vector>

class PerformanceTest {
public:
    // 비가상 함수
    void directCall() {
        // 컴파일 타임에 호출 주소 결정
        // 인라인 최적화 가능
    }
    
    // 가상 함수
    virtual void virtualCall() {
        // 런타임에 vtable 룩업 필요
        // 인라인 최적화 어려움
    }
};

class DerivedPerformance : public PerformanceTest {
public:
    void virtualCall() override {
        // 오버라이드된 구현
    }
};

void measurePerformance() {
    const int iterations = 100000000;
    
    PerformanceTest obj;
    PerformanceTest* ptr = new DerivedPerformance();
    
    // 직접 호출 측정
    auto start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < iterations; ++i) {
        obj.directCall();
    }
    auto mid = std::chrono::high_resolution_clock::now();
    
    // 가상 함수 호출 측정
    for (int i = 0; i < iterations; ++i) {
        ptr->virtualCall();
    }
    auto end = std::chrono::high_resolution_clock::now();
    
    auto directTime = std::chrono::duration_cast<std::chrono::microseconds>(mid - start);
    auto virtualTime = std::chrono::duration_cast<std::chrono::microseconds>(end - mid);
    
    cout << "직접 호출: " << directTime.count() << " μs" << endl;
    cout << "가상 호출: " << virtualTime.count() << " μs" << endl;
    cout << "성능 차이: " << (double)virtualTime.count() / directTime.count() << "배" << endl;
    
    delete ptr;
}

2. 캐시 친화적이지 않은 접근 패턴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void demonstrateCacheEffects() {
    const int count = 1000000;
    std::vector<std::unique_ptr<PerformanceTest>> objects;
    
    // 다양한 타입의 객체들을 섞어서 저장
    for (int i = 0; i < count; ++i) {
        if (i % 2 == 0) {
            objects.push_back(std::make_unique<PerformanceTest>());
        } else {
            objects.push_back(std::make_unique<DerivedPerformance>());
        }
    }
    
    auto start = std::chrono::high_resolution_clock::now();
    
    // 가상 함수 호출 - vtable 룩업으로 인한 캐시 미스 가능성
    for (auto& obj : objects) {
        obj->virtualCall();
    }
    
    auto end = std::chrono::high_resolution_clock::now();
    
    cout << "혼합 객체 가상 호출 시간: " 
         << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() 
         << " μs" << endl;
}

🎯 실무 활용 예제

1. 플러그인 시스템 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class Plugin {
public:
    virtual ~Plugin() = default;
    virtual void initialize() = 0;
    virtual void process(const std::string& input) = 0;
    virtual std::string getName() const = 0;
};

class AudioPlugin : public Plugin {
public:
    void initialize() override {
        cout << "Audio plugin initialized" << endl;
    }
    
    void process(const std::string& input) override {
        cout << "Processing audio: " << input << endl;
    }
    
    std::string getName() const override {
        return "AudioPlugin";
    }
};

class VideoPlugin : public Plugin {
public:
    void initialize() override {
        cout << "Video plugin initialized" << endl;
    }
    
    void process(const std::string& input) override {
        cout << "Processing video: " << input << endl;
    }
    
    std::string getName() const override {
        return "VideoPlugin";
    }
};

class PluginManager {
private:
    std::vector<std::unique_ptr<Plugin>> plugins;
    
public:
    void addPlugin(std::unique_ptr<Plugin> plugin) {
        plugin->initialize();
        plugins.push_back(std::move(plugin));
    }
    
    void processData(const std::string& data) {
        for (auto& plugin : plugins) {
            cout << "Using " << plugin->getName() << ": ";
            plugin->process(data); // 다형적 호출
        }
    }
};

2. 게임 엔진의 컴포넌트 시스템

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class Component {
public:
    virtual ~Component() = default;
    virtual void update(float deltaTime) = 0;
    virtual void render() = 0;
    virtual std::string getType() const = 0;
};

class TransformComponent : public Component {
private:
    float x, y, z;
    
public:
    TransformComponent(float x, float y, float z) : x(x), y(y), z(z) {}
    
    void update(float deltaTime) override {
        // 위치 업데이트 로직
    }
    
    void render() override {
        cout << "Rendering at (" << x << ", " << y << ", " << z << ")" << endl;
    }
    
    std::string getType() const override { return "Transform"; }
};

class MeshComponent : public Component {
private:
    std::string meshName;
    
public:
    MeshComponent(const std::string& name) : meshName(name) {}
    
    void update(float deltaTime) override {
        // 메시 애니메이션 업데이트
    }
    
    void render() override {
        cout << "Rendering mesh: " << meshName << endl;
    }
    
    std::string getType() const override { return "Mesh"; }
};

class GameObject {
private:
    std::vector<std::unique_ptr<Component>> components;
    
public:
    void addComponent(std::unique_ptr<Component> component) {
        components.push_back(std::move(component));
    }
    
    void update(float deltaTime) {
        for (auto& component : components) {
            component->update(deltaTime); // vptr을 통한 다형적 호출
        }
    }
    
    void render() {
        for (auto& component : components) {
            component->render(); // vptr을 통한 다형적 호출
        }
    }
};

🔎 주의점과 모범 사례

1. 생성자/소멸자에서의 vptr 동작

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class DangerousBase {
public:
    DangerousBase() {
        // 위험: 생성자에서 가상 함수 호출
        initialize(); // Base::initialize 호출 (파생 클래스 버전 아님!)
    }
    
    virtual ~DangerousBase() {
        // 위험: 소멸자에서 가상 함수 호출
        cleanup(); // Base::cleanup 호출
    }
    
    virtual void initialize() {
        cout << "Base initialization" << endl;
    }
    
    virtual void cleanup() {
        cout << "Base cleanup" << endl;
    }
};

class DangerousDerived : public DangerousBase {
public:
    DangerousDerived() {
        cout << "Derived constructor" << endl;
    }
    
    ~DangerousDerived() {
        cout << "Derived destructor" << endl;
    }
    
    void initialize() override {
        cout << "Derived initialization" << endl; // 호출되지 않음!
    }
    
    void cleanup() override {
        cout << "Derived cleanup" << endl; // 호출되지 않음!
    }
};

// 안전한 패턴
class SafeBase {
public:
    SafeBase() = default;
    virtual ~SafeBase() = default;
    
    // 초기화는 생성 후 별도로 호출
    virtual void initialize() = 0;
    virtual void cleanup() = 0;
};

class SafeDerived : public SafeBase {
public:
    void initialize() override {
        cout << "Safe derived initialization" << endl;
    }
    
    void cleanup() override {
        cout << "Safe derived cleanup" << endl;
    }
};

2. 성능 최적화 기법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 1. 가상 함수 호출 최소화
class OptimizedRenderer {
private:
    std::vector<Component*> transformComponents;
    std::vector<Component*> meshComponents;
    
public:
    void addComponent(Component* component) {
        // 타입별로 분류하여 저장
        if (component->getType() == "Transform") {
            transformComponents.push_back(component);
        } else if (component->getType() == "Mesh") {
            meshComponents.push_back(component);
        }
    }
    
    void renderBatch() {
        // 같은 타입끼리 배치 처리 - 분기 예측 향상
        for (auto* component : transformComponents) {
            component->render();
        }
        for (auto* component : meshComponents) {
            component->render();
        }
    }
};

// 2. 템플릿을 이용한 정적 다형성 (vptr 오버헤드 없음)
template<typename T>
class StaticPolymorphism {
public:
    void execute() {
        static_cast<T*>(this)->implementation();
    }
};

class FastImplementation : public StaticPolymorphism<FastImplementation> {
public:
    void implementation() {
        cout << "Fast implementation without vptr overhead" << endl;
    }
};

🌐 외부 링크


💡 실무 적용 팁

  1. 가상 함수 사용 판단 기준:
    • 다형적 동작이 실제로 필요한가?
    • 성능보다 유연성이 중요한가?
    • 플러그인이나 확장 가능한 시스템인가?
  2. 메모리 최적화:
    • 작은 객체에서는 vptr 오버헤드 고려
    • 배열에서는 같은 타입끼리 배치 처리
    • 필요시 객체 풀링으로 할당 비용 감소
  3. 성능 최적화:
    • 핫패스에서는 템플릿 기반 정적 다형성 고려
    • 가상 함수 호출을 배치로 묶어서 처리
    • 프로파일링으로 실제 병목 지점 확인

다음 학습 주제

  • 고급 다형성 기법: CRTP(Curiously Recurring Template Pattern), Policy-based Design
  • 메모리 최적화: 객체 풀링, SoA vs AoS, 커스텀 할당자
  • 현대 C++ 대안: std::variant, std::function, 컨셉과 타입 소거
  • 어셈블리 레벨 분석: 가상 함수 호출의 저수준 메커니즘

🪞 회고 질문

  • vptr이 객체의 어느 위치에 저장되고 언제 초기화되는지 정확히 이해하고 있는가?
  • 가상 함수의 성능 오버헤드를 정량적으로 측정하고 최적화할 수 있는가?
  • 다형성이 필요한 상황과 그렇지 않은 상황을 구별할 수 있는가?
  • 생성자와 소멸자에서 가상 함수 호출의 위험성을 이해하고 있는가?
This post is licensed under CC BY 4.0 by the author.