Post

C++ 다이아몬드 상속 문제와 가상 상속 해결책

📌 학습 목표

  • 다이아몬드 상속 문제의 발생 원인과 증상 이해
  • 가상 상속을 통한 해결 방법 학습
  • 메모리 레이아웃과 성능 영향 분석
  • 컴포지션을 활용한 대안 설계 방법 습득

📝 다이아몬드 상속 문제란?

문제 정의

다중 상속 시 공통 조상 클래스가 중복 포함되어 발생하는 문제

  • 메모리 중복: 같은 기본 클래스가 여러 번 포함
  • 호출 모호성: 어떤 기본 클래스의 멤버인지 불분명
  • 일관성 문제: 같은 데이터가 여러 복사본으로 존재

문제 상황 예제

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
66
67
68
#include <iostream>
using namespace std;

class Animal {
protected:
    string name;
    int age;
    
public:
    Animal(const string& n, int a) : name(n), age(a) {
        cout << "Animal constructor: " << name << endl;
    }
    
    void eat() {
        cout << name << " is eating" << endl;
    }
    
    void displayInfo() {
        cout << "Name: " << name << ", Age: " << age << endl;
    }
};

class Mammal : public Animal {
public:
    Mammal(const string& name, int age) : Animal(name, age) {
        cout << "Mammal constructor" << endl;
    }
    
    void breathe() {
        cout << name << " is breathing air" << endl;
    }
};

class WingedAnimal : public Animal {
public:
    WingedAnimal(const string& name, int age) : Animal(name, age) {
        cout << "WingedAnimal constructor" << endl;
    }
    
    void fly() {
        cout << name << " is flying" << endl;
    }
};

// 문제 발생 클래스
class Bat : public Mammal, public WingedAnimal {
public:
    Bat(const string& name, int age) 
        : Mammal(name, age), WingedAnimal(name, age) {  // Animal이 두 번 생성됨!
        cout << "Bat constructor" << endl;
    }
    
    void hunt() {
        cout << "Bat is hunting insects" << endl;
    }
};

void demonstrateProblem() {
    cout << "=== 다이아몬드 상속 문제 시연 ===" << endl;
    
    // Bat bat("Vampire", 3);  // 컴파일 에러 또는 모호성 발생
    
    // 문제들:
    // 1. Animal 생성자가 두 번 호출됨
    // 2. name과 age가 두 복사본 존재
    // 3. bat.eat(); // 어떤 Animal의 eat()인지 모호함
    // 4. bat.displayInfo(); // 컴파일 에러
}

💻 가상 상속을 통한 해결

가상 상속 문법

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
class Animal {
protected:
    string name;
    int age;
    static int animalCount;
    
public:
    Animal(const string& n, int a) : name(n), age(a) {
        animalCount++;
        cout << "Animal constructor: " << name 
             << " (총 Animal 객체: " << animalCount << ")" << endl;
    }
    
    virtual ~Animal() {
        animalCount--;
        cout << "Animal destructor: " << name 
             << " (남은 Animal 객체: " << animalCount << ")" << endl;
    }
    
    virtual void eat() {
        cout << name << " is eating" << endl;
    }
    
    void displayInfo() {
        cout << "Name: " << name << ", Age: " << age << endl;
    }
};

int Animal::animalCount = 0;

// 가상 상속 사용
class Mammal : virtual public Animal {
public:
    Mammal(const string& name, int age) : Animal(name, age) {
        cout << "Mammal constructor" << endl;
    }
    
    virtual ~Mammal() {
        cout << "Mammal destructor" << endl;
    }
    
    void breathe() {
        cout << name << " is breathing air" << endl;
    }
};

class WingedAnimal : virtual public Animal {
public:
    WingedAnimal(const string& name, int age) : Animal(name, age) {
        cout << "WingedAnimal constructor" << endl;
    }
    
    virtual ~WingedAnimal() {
        cout << "WingedAnimal destructor" << endl;
    }
    
    void fly() {
        cout << name << " is flying" << endl;
    }
};

class Bat : public Mammal, public WingedAnimal {
public:
    // 가상 상속 시 가장 파생된 클래스에서 가상 기본 클래스 초기화
    Bat(const string& name, int age) 
        : Animal(name, age),  // 직접 초기화 필요
          Mammal(name, age), 
          WingedAnimal(name, age) {
        cout << "Bat constructor" << endl;
    }
    
    ~Bat() {
        cout << "Bat destructor" << endl;
    }
    
    void hunt() {
        cout << name << " is hunting insects" << endl;
    }
    
    void echolocate() {
        cout << name << " is using echolocation" << endl;
    }
};

void demonstrateSolution() {
    cout << "=== 가상 상속 해결책 시연 ===" << endl;
    
    Bat bat("Vampire", 3);
    
    cout << "\n=== 메서드 호출 ===" << endl;
    bat.eat();         // 모호성 없음 - Animal::eat() 호출
    bat.breathe();     // Mammal::breathe() 호출
    bat.fly();         // WingedAnimal::fly() 호출
    bat.hunt();        // Bat::hunt() 호출
    bat.displayInfo(); // Animal::displayInfo() 호출
    
    cout << "\n=== 다운캐스팅 테스트 ===" << endl;
    Animal* animalPtr = &bat;
    animalPtr->eat();
    
    cout << "\n=== 객체 소멸 시작 ===" << endl;
}

🔍 메모리 레이아웃 분석

일반 상속 vs 가상 상속

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
#include <cstdint>

class MemoryAnalysis {
public:
    static void analyzeLayout() {
        cout << "=== 메모리 레이아웃 분석 ===" << endl;
        
        cout << "Animal 크기: " << sizeof(Animal) << " bytes" << endl;
        cout << "Mammal 크기: " << sizeof(Mammal) << " bytes" << endl;
        cout << "WingedAnimal 크기: " << sizeof(WingedAnimal) << " bytes" << endl;
        cout << "Bat 크기: " << sizeof(Bat) << " bytes" << endl;
        
        Bat bat("Test", 1);
        
        cout << "\n=== 객체 주소 분석 ===" << endl;
        cout << "Bat 객체 주소: " << &bat << endl;
        
        // 기본 클래스로의 암시적 변환
        Animal* animalPtr = &bat;
        Mammal* mammalPtr = &bat;
        WingedAnimal* wingedPtr = &bat;
        
        cout << "Animal* 주소: " << animalPtr << endl;
        cout << "Mammal* 주소: " << mammalPtr << endl;
        cout << "WingedAnimal* 주소: " << wingedPtr << endl;
        
        // 가상 상속에서는 모든 포인터가 같은 Animal을 가리킴
        cout << "\n=== 가상 기본 클래스 확인 ===" << endl;
        cout << "Animal 포인터들이 같은 객체를 가리키는가: " 
             << (animalPtr == static_cast<Animal*>(mammalPtr)) << endl;
    }
};

성능 고려사항

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
class PerformanceComparison {
public:
    static void comparePerformance() {
        const int iterations = 1000000;
        
        cout << "=== 성능 비교 ===" << endl;
        
        auto start = chrono::high_resolution_clock::now();
        
        // 가상 상속 객체 생성/소멸
        for (int i = 0; i < iterations; ++i) {
            Bat bat("Test", 1);
            bat.eat();
        }
        
        auto end = chrono::high_resolution_clock::now();
        auto duration = chrono::duration_cast<chrono::microseconds>(end - start);
        
        cout << "가상 상속 처리 시간: " << duration.count() << " μs" << endl;
        
        // 가상 상속의 오버헤드:
        // 1. 생성자 호출 순서 복잡성
        // 2. 가상 기본 클래스 포인터 관리
        // 3. 메모리 레이아웃 복잡성
    }
};

🎯 컴포지션을 활용한 대안 설계

상속 대신 컴포지션 사용

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
class AnimalTraits {
private:
    string name;
    int age;
    
public:
    AnimalTraits(const string& n, int a) : name(n), age(a) {}
    
    void eat() {
        cout << name << " is eating" << endl;
    }
    
    void displayInfo() {
        cout << "Name: " << name << ", Age: " << age << endl;
    }
    
    const string& getName() const { return name; }
    int getAge() const { return age; }
};

class MammalBehavior {
private:
    AnimalTraits& traits;
    
public:
    MammalBehavior(AnimalTraits& t) : traits(t) {}
    
    void breathe() {
        cout << traits.getName() << " is breathing air" << endl;
    }
};

class FlyingBehavior {
private:
    AnimalTraits& traits;
    
public:
    FlyingBehavior(AnimalTraits& t) : traits(t) {}
    
    void fly() {
        cout << traits.getName() << " is flying" << endl;
    }
};

// 컴포지션 기반 Bat 클래스
class CompositionBat {
private:
    AnimalTraits traits;
    MammalBehavior mammalBehavior;
    FlyingBehavior flyingBehavior;
    
public:
    CompositionBat(const string& name, int age)
        : traits(name, age),
          mammalBehavior(traits),
          flyingBehavior(traits) {
        cout << "CompositionBat constructor" << endl;
    }
    
    // 인터페이스 위임
    void eat() { traits.eat(); }
    void breathe() { mammalBehavior.breathe(); }
    void fly() { flyingBehavior.fly(); }
    void displayInfo() { traits.displayInfo(); }
    
    void hunt() {
        cout << traits.getName() << " is hunting insects" << endl;
    }
};

void demonstrateComposition() {
    cout << "=== 컴포지션 기반 설계 ===" << endl;
    
    CompositionBat bat("Composition Bat", 2);
    
    bat.eat();
    bat.breathe();
    bat.fly();
    bat.hunt();
    bat.displayInfo();
    
    cout << "CompositionBat 크기: " << sizeof(CompositionBat) << " bytes" << endl;
}

💡 실무 적용 가이드라인

다이아몬드 상속 문제 해결 전략

  1. 가상 상속 사용 (신중하게)
  2. 컴포지션으로 재설계
  3. 인터페이스 분리
  4. 믹스인 패턴 활용

선택 기준

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
// 1. 진짜 "is-a" 관계인가?
class Bird {};
class Mammal {};
class Bat : public Bird, public Mammal {};  // 잘못된 설계

// 2. 인터페이스 기반 설계
class IFlyable {
public:
    virtual void fly() = 0;
    virtual ~IFlyable() = default;
};

class IBreathable {
public:
    virtual void breathe() = 0;
    virtual ~IBreathable() = default;
};

class BetterBat : public Animal, public IFlyable, public IBreathable {
public:
    BetterBat(const string& name, int age) : Animal(name, age) {}
    
    void fly() override {
        cout << "Bat flying with wings" << endl;
    }
    
    void breathe() override {
        cout << "Bat breathing air" << endl;
    }
};

🌐 외부 링크


다음 학습 주제

  • 디자인 패턴: 믹스인 패턴, 어댑터 패턴을 통한 다중 상속 대안
  • C++20 컨셉: 인터페이스 설계의 새로운 방법
  • CRTP 패턴: 템플릿을 활용한 정적 다형성

🪞 회고 질문

  • 다이아몬드 상속 문제의 근본 원인을 정확히 이해하고 있는가?
  • 가상 상속의 메모리 오버헤드와 복잡성을 감수할 만한 상황을 판단할 수 있는가?
  • 상속보다 컴포지션이 더 적합한 상황을 구별할 수 있는가?
This post is licensed under CC BY 4.0 by the author.