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
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.