[C++] 다중 상속의 문제점 : 다이아몬드 문제 (Diamond Problem)
다중 상속
정의
다중 상속이란, 하나의 클래스가 두 개 이상의 클래스로부터 멤버를 상속받아 파생 클래스를 생성하는 것을 의미합니다.
다중 상속의 문제점
1. 상속받은 여러 기초 클래스에 같은 이름의 멤버가 존재하는 경우
2. 하나의 클래스를 간접적으로 두 번 이상 상속받을 경우
3. 가상 클래스가 아닌 기초 클래스를 다중 상속하여, 기초 클래스 타입의 포인터로 파생 클래스를 가리킬 수 없는 경우
다중 상속을 사용하는 경우 위와 같은 상황이 벌어질 수 있고, 이런 경우 "모호한(ambiguous) 호출"같은 오류가 발생할 수 있습니다.
다이아몬드 상속 구조
다중 상속을 다이아몬드 구조로 사용한 경우 '다중 상속의 모호성 문제'가 발생하게 됩니다.
위 그림과 같이 A, B, C, D 클래스의 구조가 있을 때, D 클래스 객체를 통해 A 클래스 함수를 호출하면 B에 구현한 함수와 C에 구현한 함수 중, 어느 함수를 선택해야 할지 모르기 때문에 "모호함" 문제가 발생합니다.
또한, D의 메모리를 확인하면 A 타입의 메모리가 하나만 존재할 것 같지만, 실제론 D::B::A와, D::C::A의 2개 메모리가 생깁니다.
즉, 다이아몬드 구조로 다중 상속을 하게되면, 손자 객체(D)에 할머니 객체 2개(B->A, C->A)가 생기는 것입니다.
클래스 D 객체를 디버깅해서 확인해보면 B::A의 가상함수 테이블과 C::A의 가상함수 테이블의 주소가 각각 달라 별도의 메모리가 할당됨을 확인할 수 있었습니다.
해결방법
다이아몬드 구조의 다중상속을 구현할 때, virtual로 '가상 상속'을 받으면 상속 시 부모 타입의 메모리가 중복되지 않으며 상속됩니다.
단, 다중 상속 하는 손자 클래스의 경우, 생성자가 있다면 할머니 클래스의 생성자를 꼭 직접 호출해주어야 합니다.
(ex. D(int a) : B(a), C(a), A(a))
클래스 B와 클래스 C를 각각 "virtual public A" 로 A 클래스를 가상상속 받아주었더니, 클래스 D의 가상함수 테이블이 전부 동일한 주소를 나타냈습니다.
가상 상속 (Virtual Inheritance)
가상 상속은 C++의 특징 중 하나로, 다중 상속을 사용할 때 발생할 수 있는 문제를 해결하기 위한 방법입니다. 특히 위에서처럼 '다이아몬드 상속 문제'를 해결하는데 사용됩니다.
가상 상속을 통해 클래스를 상속받으면, 중복 상속을 피하고 단일 복사본을 유지할 수 있습니다.
즉, 가상 상속을 사용하면 여러 개의 서브클래스가 같은 베이스 클래스를 상속받더라도, 그 베이스 클래스의 인스턴스는 단 한번만 파생 클래스에 포함되는것입니다. 이는 컴파일러가 가상 베이스 클래스의 인스턴스를 관리하는 방식 때문입니다.
가상 상속의 메모리 구조 : vbptr (가상 베이스 테이블)
일반적인 상속에서, 각 서브 클래스는 베이스 클래스의 복사본을 포함하게 됩니다. 그러나 가상 상속에서, 베이스 클래스는 가상 베이스 테이블(virtual base table)이라는 것을 통해 관리됩니다. 가상 베이스 테이블은 파생 클래스의 객체마다 하나씩 생성되며, 가상 베이스 클래스의 인스턴스에 대한 포인터를 저장합니다.
이런 가상 베이스 테이블을 통해, 파생 클래스의 객체는 각자 가상 베이스 클래스의 인스턴스를 참조하게 됩니다. 그래서 여러 서브 클래스가 같은 베이스 클래스를 상속받아도, 베이스 클래스의 인스턴스는 단 한 번만 생성되어 파생 클래스에 포함되는 것입니다.
C++ 언어의 다중상속
C++언어는 다중 상속을 허용하여 언어의 유연성과 표현력을 향상시켰습니다. C++의 다중 상속은 여러 기초 클래스의 특징과 기능을 한 번에 상속받아 사용하여 보다 복잡한 클래스 구조를 구현하는데 도움을 줍니다.
또한, 다중 상속은 인터페이스와 구현을 분리하는 데 유용합니다. 클래스가 여러 인터페이스를 상속받아 구현할 수 있게되어, 코드의 재사용성이 향상되고 객체지향설계 원칙에 더 부합하게 됩니다.
C++이 아닌 Java나 C# 같은 다른 객체지향언어는 다중 상속을 지원하지 않는 대신, 인터페이스를 통한 다중 구현을 지원합니다.
인터페이스 다중 상속
인터페이스는 어떠한 구현도 포함되어있지 않기 때문에 다중으로 상속받아도 구현 간의 충돌이나 모호성 문제가 발생하지 않습니다. 같은 이름과 시그니처를 가진 메서드가 여러 인터페이스에 선언된 경우에도 문제가 발생할 것 같지만, 문제는 발생하지 않습니다. 인터페이스에 있는 메서드들은 모두 추상 메서드로, 실제 구현 내용이 없기 때문입니다.
따라서 동일한 이름과 시그니처를 가진 메서드는 클래스에서 필요한 로직을 한 번만 제공하면 됩니다.
위 코드를 보면 A 인터페이스와, B 인터페이스에 동일한 이름인 doSomething() 메서드가 있습니다. 그러나 두 인터페이스를 다중 상속 받는 클래스 C에서는 doSomething() 이라는 이름을 갖는 하나의 메서드만 정의하여 사용하면 됩니다.