다중상속(Multiple Inheritance: MI) 에 대해 좋다/나쁘다 의견차이가 있고 논쟁이 있다.
다중상속은 복잡도가 늘어나고 문제의 여지가 있지만 적법하게 사용될 수 있는 시나리오가 있기 때문에 이에 대해 검토가 필요하다.
모호성 문제
둘 이상의 기본 클래스로부터 똑같은 이름(ex. 함수, typedef 등)을 물려받을 가능성이 존재하고 이로 인해 모호성 발생한다.
class BorrowableItem{
public:
void checkOut();
...
};
class ElectronicGadget{
private:
bool checkOut() const;
...
};
class MP3Player: public BorrowableItem, public ElectronicGadget
{ ... };
MP3Player mp;
mp.checkOut();
상속받은 객체에 같은 이름의 함수가 존재하기 때문에 mpdml checkOut()에 대해서 모호성이 발생한다.
심지어 ElectronicGadget 객체에서 private 으로 설정되어 있어 BorrowableItem::checkOut()임이 분명한대도 컴파일러의 함수매치 알고리즘에 의해 모호성이 발생하고 컴파일에러를 발생시킨다.
deadly MI diamond
class File { ... };
class InputFile: public File { ... };
class OutputFile: public File { ... };
class IOFile: public InputFile, public OutputFile { ... };
상속관계를 보면 InputFile과 OutputFile 모두 File을 상속받고 있고 IOFile이 이를 다시 받고 있다.
IOFile 입장에서는 File 에 대해서 InputFile, OutputFile로부터 상속받아 중복적인 데이터가 존재하는 문제가 있다.
중복데이터를 원하지 않는 경우 해결 방법 : 가상 기본 클래스(virtual basic class)로 만든다.
class File { ... };
class InputFile: virtual public File { ... };
class OutputFile: virtual public File { ... };
class IOFile: public InputFile, public OutputFile { ... };
- 표준 라이브러리 : basic_ios, basic_istream, basic_ostream, basic_iostream
virtual 상속을 쓰는 경우 단점
- 일반적으로 객체 크기가 더 큰 단점이 있다.
- 가상 기본 클래스의 데이터 멤버에 접근하는 속도도 비가상 기본 클래스의 데이터 멤버 접근 속도보다 느리다.
- 초기화 규칙의 복잡성
1) 파생 클래스는 가상 기본 클래스와의 거리와 상관없이 가상 기본 클래스의 존재를 염두해 두고 있어야 한다.
2) 기존 클래스 계통에 파생 클래스를 새로 추가할 때도 그 파생클래스는 가상 기본 클래스의 초기화를 떠맡아야 한다.
virtual 상속의 단점 해결하기
- 가상 기본 클래스를 사용하지 않는다.
- 가상 기본 클래스를 정말 써야한다면 기본 클래스에는 데이터를 넣지 않는다.
자바, 닷넷의 Interface와 비교되는 개념.
다중 상속을 적법하게 쓸 수 있는 시나리오 예시
인터페이스 클래스로부터 public 상속을 시킴돠 동시에 구현을 돕는 클래스로부터 private 상속을 시키는 방식.
class IPerson{
public:
virtual ~IPerson();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
};
// 유일한 데이터베이스 ID로부터 IPerson 객체를 만들어내는 팩토리 함수.
std::tr1::shared_ptr<IPerson> makePerson(DatabaseID personIdentifier);
// 사용자로부터 데이터베이스 ID를 얻어내는 함수
DatabaseId askUserForDatabaseId();
DatabaseId id(askUserForDatabaseId());
std::tr1::shared_ptr<IPerson> pp(makePerson(id));
...
IPerson은 사람이 가지고 있는 인터페이스를 구현해놓은 클래스이다.
팩토리 함수를 이용해 구현된 사람은 사람에 대한 구현부 내용을 가지고 있어야 하고 아래 PersonInfo 클래스가 그 내용을 가지고 있다고 해보자.
class PersonInfo{
public:
explicit PersonInfo(DatabaseID pid);
virtual ~PersionInfo();
virtual const char* theName() const;
virtual const char* theBirthDate() const;
...
private:
virtual const char* valueDelimOpen() const; // 다양한 서식 출력
virtual const char* valueDelimClose() const; //다양한 서식 출력
...
};
PersonInfo 의 theName은 다양한 방식의 출력을 제공하기 위해 private 함수인 valueDelimOpen(), valueDelimClose()를 이용하게 된다.
CPerson은 사람 객체를 만드는 클래스인데 PersonInfo를 이용해 구현되게 된다.
CPerson과 PersonInfo 사이관계는 'is-implemented-in-terms-of' 관계인 것이다.
이를 구성하기 위해 composition을 이용하려고 해봤지만 CPerson에서는 valueDelimOpen(), valueDelimClose() 를 반드시 재정의 해야하기 때문에 private 상속을 이용할 수 밖에 없다.
class CPerson: public IPerson, private PersonInfo{
public:
explicit CPersson(DatabaseID pid): PersonInfo(pid){}
virtual std::string name() const
{ return PersonInfo::theName(); }
virtual std::string birthDate() const
{ return PersonInfo::theBirthDate() }
private:
const char * valueDelimOpen() const { return " "; }
const char * valueDelimClose() const { return " "; }
결론
단일 상속으로 동등 효과를 낼 수 있다면 단일 상속으로 가는 것이 좋다.
다중 상속에 알맞는 시나리오도 존재하니 확신이 드는 다중 상속 설계는 이용하자.
댓글