[Effective C++] 다중 상속은 심사숙고해서 사용하자
항목 40 : 다중 상속은 심사숙고해서 사용하자
다중 상속(multiple inheritance: MI)에 대해 꼭 기억하고 있어야 할 점중 하나는, 둘 이상의 기본 클래스로부터 똑같은 이름(함수, typedf 등)을 물려받을 가능성이 생겨 버린다는 점입니다. 다중 상속 때문에 모호성이 생긴다는 것이죠. 아래의 예시를 보시죠.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class BorrowableItem { // 라이브러리로부터 여러분이 가져올 수 있는 어떤 것
public :
void checkOut(); // 라이브러리로부터 체크아웃합니다.
...
};
class ElectronicGadget {
private:
bool checkOut() const; // 자체 테스트를 실시하고, 성공 여부를 반환합니다.
...
};
class MP3Player:public BorrowableItem, public ElectronicGadget {...}; // 다중 상속
MP3Player mp;
mp.checkOut(); // 어떤 checkout을 호출하는지 알 수 없습니다!
|
checkout을 호출하는 시점에서 모호성이 발생합니다 여기서 주목해야할 점은 사실 접근할 수 있는 checkout는 하나밖에 없는데도 불구하고 모호성이 생긴다는 점입니다. 이것은 중복된 함수 호출 중 하나를 골라내는 C++의 규칙을 따른 결과입니다. 어떤 함수가 접근 가능한 함수인지를 알아보기 전에, C++ 컴파일러는 이 규칙을 써서 주어진 호출에 대해 최적으로 일치하는 함수인지를 먼저 확인합니다. 지금의 경우 checkout 함수는 C++ 규칙에 의한 일치도가 서로 같기 때문에, 최적 일치 함수가 결정되지 않습니다.
이러한 모호성을 해소하려면, 호출할 기본 클래스의 함수를 손수 지정해 주어야 합니다.
1
|
mp.BorrowableItem::checkOut();
|
다중 상속의 의미는 '둘 이상의 클래스로부터 상속을 받는 것'일 뿐이지만, 이 MI는 상위 단계의 기본 클래스를 여러 개 갖는 클래스 계통에서 심심치 않게 눈에 띕니다. 이런 구조의 계통에서는 소위 "죽음의 MI 마름모꼴"이라고 알려진 좋지 않은 모양이 나올 수 있습니다
1
2
3
4
5
6
7
|
class File {...};
class InputFile: public File {...};
class OutputFile: public File {...};
class IOFile: public InputFile, public OutputFile {...};
|

이렇게 기본 클래스와 파생 클래스 사이의 경로가 두 개 이상이 되는 상속 계통(위 그림의 File 및 IOFile의 사이에 InputFile 및 OutputFile을 거쳐 가는 경로가 한 개씩 있는 경우처럼)을 혹시라도 쓰게 되면, 기본 클래스의 데이터 멤버가 경로 개수만큼 중복 생성되게 됩니다.
File 클래스 안에 fileName이라는 데이터 멤버가 하나 들어 있다고 가정했을때, IOFile 클래스에는 이 필드가 몇 개가 들어 있어야 할까요?
기본 클래스로부터 사본을 하나씩 물려받게 되니까 결과적으로 fileName 데이터 멤버가 두 개이어야 할 것도 같고, 단순하게 봤을때 IOFile 객체는 파일 이름이 하나만 있는 게 맞으니까, 두 기본 클래스로부터 fileName을 동시에 물려받더라도 fileName이 중복되면 안 될 것도 같습니다.
C++는 두 가지를 모두 지원합니다. 기본적으로는 데이터 멤버를 중복생성하는 쪽이지만요.
만약 데이터 멤버의 중복 생성을 원하는 것이 아니라면, 해당 데이터 멤버를 가진 클래스를 가상 기본 클래스로 만드는 것으로 해결을 볼 수 있습니다. 더 자세히 말하자면, 가상 기본 클래스로 삼을 클래스에 직접 연결된 파생 클래스에서 가상 상속을 사용하게 만드는 것입니다.
1
2
3
4
5
6
7
|
class File {...};
class InputFile: virtual public File {...};
class OutputFile: virtual public File {...};
class IOFile: public InputFile, public OutputFile {...};
|
정확한 동작의 관점에서 보면, public 상속은 반드시 항상 가상 상속이어야 하는 것이 맞습니다.
상속되는 데이터 멤버의 중복생성을 막는 데는 우리 눈에는 보이지 않는 컴파일러의 숨은 꼼수가 필요합니다.
그리고 그 꼼수 덕택에, 가상 상속을 사용하는 클래스로 만들어진 객체는 가상 상속을 쓰지 않은 것보다 일반적으로 크기가 더 큽니다. 게다가 가상 기본 클래스의 데이터 멤버에 접근하는 속도도 비가상 기본 클래스의 데이터 멤버에 접근하는 속도보다 느립니다. 세부적인 크기, 속도 차이는 컴파일러마다 다르지만, 이 점만은 분명히 말할 수 있습니다. 가상 상속은 비쌉니다.
비용지불이 이것으로 끝나는게 아닙니다.
가상 기본 클래스의 초기화에 관련된 규칙은 비가상 기본 클래스의 초기화 규칙보다 훨씬 복잡한데다가 직관성도 더 떨어집니다. 대부분의 경우, 가상 상속이 되어 있는 클래스 계통에서는 파생 클래스들로 인해 가상 기본 클래스 부분을 초기화할 일이 생기게 됩니다. 이때 들어가는 초기화 규칙은 다음과 같습니다.
- 초기화가 필요한 가상 기본 클래스로부터 클래스가 파생된 경우, 이 파생 클래스는 가상 기본 클래스와의 거리에 상관없이 가상 기본 클래스의 존재를 염두에 두고 있어야 합니다.
- 기존의 클래스 계통에 파생 클래스를 새로 추가할 때도 그 파생 클래스는 가상 기본클래스의 초기화를 떠맡아야 합니다.
가상 기본 클래스에 대한 조언은 간단합니다.
- 구태여 쓸 필요가 없으면 가상 기본 클래스를 사용하지 마세요.
- 가상 기본 클래스를 정말 쓰지 않으면 안 될 상황이라면, 가상 기본 클래스에는 데이터를 넣지 않는 쪽으로 최대한 신경을 쓰세요.
다중 상속을 적법하게 쓸 수 있는 경우가 있습니다.
인터페이스 클래스로부터 public 상속을 시킴과 동시에 구현을 돕는 클래스로부터 private 상속을 시키는 것입니다.
꼭 잊지 말아야 할 것!
1. 다중 상속은 단일 상속보다 확실히 복잡합니다. 새로운 모호성 문제를 일으킬 뿐만 아니라 가상 상속이 필요해질 수도 있습니다.
2. 가상 상속을 쓰면 크기 비용, 속도 비용이 늘어나며, 초기화 및 대입 연산의 복잡도가 커집니다. 따라서 가상 기본 클래스에는 데이터를 두지 않는 것이 현실적으로 가장 실용적입니다.
3. 다중 상속을 적법하게 쓸 수 있는 경우가 있습니다. 여러 시나리오 중 하나는, 인터페이스 클래스로부터 public 상속을 시킴과 동시에 구현을 돕는 클래스로부터 private 상속을 시키는 것입니다.