10장 : 객체 지향 프로그래밍 심화
다중 상속과 가상 상속
이전 절 ‘상속의 개념과 구현’에서 다중 상속을 간단히 언급했습니다.
이번 절에서는 다중 상속에서 가장 중요한 이슈인 다이아몬드 상속 문제와, 이를 해결하는 가상 상속(virtual inheritance)을 실제 코드로 다룹니다.
다이아몬드 상속 문제란?
다음과 같은 상속 구조를 생각해봅시다.
Device를Printer,Scanner가 상속Copier가Printer,Scanner를 모두 상속
이 구조는 상속 그래프가 마름모(다이아몬드) 형태를 이루며, Device가 중복으로 포함될 수 있습니다.
#include <iostream>
class Device {
public:
int id;
Device() : id(100) {}
};
class Printer : public Device {};
class Scanner : public Device {};
class Copier : public Printer, public Scanner {};
int main() {
Copier c;
// c.id = 10; // 오류: Device::id가 두 경로로 존재하여 모호
}Copier 안에 Device가 두 개 생기므로, 어떤 id를 의미하는지 컴파일러가 판단할 수 없습니다.
가상 상속으로 문제 해결하기
기반 클래스 쪽 상속을 virtual로 선언하면, 최종 파생 클래스에서 공유된 하나의 기반 클래스 인스턴스를 사용하게 됩니다.
#include <iostream>
class Device {
public:
int id;
Device(int v = 0) : id(v) {
std::cout << "Device ctor\n";
}
};
class Printer : virtual public Device {
public:
Printer() { std::cout << "Printer ctor\n"; }
};
class Scanner : virtual public Device {
public:
Scanner() { std::cout << "Scanner ctor\n"; }
};
class Copier : public Printer, public Scanner {
public:
Copier() : Device(42) { // 가상 기반 클래스는 최종 파생 클래스에서 초기화
std::cout << "Copier ctor\n";
}
};
int main() {
Copier c;
c.id = 7; // 모호성 없음
std::cout << "id=" << c.id << "\n";
}핵심은 Printer, Scanner에서 virtual public Device를 쓰는 것입니다.
생성자 호출 순서와 초기화 규칙
가상 상속에서는 생성자 초기화 규칙이 일반 상속과 다릅니다.
- 가상 기반 클래스(
Device)는 최종 파생 클래스(Copier)가 초기화합니다. - 중간 클래스(
Printer,Scanner)에서Device(...)를 적어도 최종 객체 생성 시 무시될 수 있습니다.
즉, 가상 기반 클래스 초기화 책임은 최종 파생 클래스에 있다고 기억하면 됩니다.
언제 다중 상속을 써야 할까?
다중 상속은 가능하지만 설계 복잡도를 크게 높입니다.
권장되는 경우
여러 인터페이스 성격의 추상 클래스를 동시에 구현해야 하는 경우.
주의가 필요한 경우
상태(데이터 멤버)를 가진 구체 클래스들을 다중 상속할 때.
대안
상속 대신 합성(Composition)으로 기능을 조합하는 것이 더 단순한 경우가 많습니다.
다중 상속 적용 기준 정리
- 다중 상속은 다이아몬드 문제를 유발할 수 있다.
virtual inheritance는 공유 기반 클래스 하나만 유지해 모호성을 제거한다.- 가상 기반 클래스 초기화는 최종 파생 클래스가 담당한다.
- 설계 복잡도가 높아지므로, 필요성과 대안을 함께 검토해야 한다.