[Effective C++] 40. 다중 상속은 심사숙고해서 사용하자
다중 상속의 의미
둘 이상의 클래스로부터 상속을 받는 것
다중 상속의 문제점
함수 호출의 모호성
둘 이상의 기본 클래스로부터 똑같은 이름(함수, typedef 등)을 물려받을 가능성이 생긴다는 점이다.
즉, 모호성이 생긴다는 것인데 아래의 예제를 봐보자.
class BorrowableItem
{
public:
void checkOut();
...
};
class ElectronicGadget
{
private:
bool checkOut() const;
...
};
class MP3Player:public BorrowableItem, public ElectronicGadget
{...};
MP3Player mp;
mp.checkOut(); // 모호성 발생!
이것은 중복된 함수 호출 중 하나를 골라내는 C++의 규칙을 따른 결과이다. 어떤 함수가 접근 가능한 함수인지를 알아보기 전에, C++ 컴파일러는 이 규칙을 써서 주어진 호출에 대해 최적으로 일치하는 함수인지 먼저 확인한다. 지금의 경우 C++ 규칙에 따른 일치도가 서로 같기 때문에, 최적 일치 함수가 결정되지 않아 호출되어야 하는 함수에 대한 모호성이 발생하게 된다.
이러한 모호성을 해결하려면 호출할 기본 클래스의 함수를 직접 지정해주어야 하는데, 이러한 방법은 깔끔하지 않다.
다이아몬드 상속
class File {...};
class InputFile:public File {...};
class OutputFile:public File {...};
class IOFile:public InputFile, public OutputFile {...};
만약 File 클래스 안에 FileName이라는 데이터 멤버가 하나 들어있다고 생각해보자. IOFile에는 이 필드가 몇 개 들어있어야 할 것인가?
C++에서는
1. InputFile, OutputFile로부터 사본을 하나씩 물려받아 결과적으로 fileName이 두 개인 버전
2. IOFile 객체는 파일 이름이 하나만 있는 게 맞으니 하나만 있는 버전
두 가지 모두 지원한다.
기본적으로는 데이터 멤버를 중복 생성하는 쪽이지만, 데이터 멤버의 중복 생성을 원한 것이 아니었다면 해당 데이터 멤버를 가진 클래스를 가상 기본 클래스로 만드는 것으로 해결할 수 있다.
// 가상 상속을 사용하여 데이터 멤버의 중복 생성을 막는 예
class File {...};
class InputFile:virtual public File {...};
class OutputFile:virtual public File {...};
class IOFile:public InputFile, public OutputFile {...};
하지만 상속되는 데이터 멤버의 중복 생성을 막는 데는 비용이 드는데, 가상 상속을 사용하는 클래스로 만들어진 객체는 가상 상속을 쓰지 않은 것보다 일반적으로 크기가 더 크다. 그리고 가상 기본 클래스의 데이터 멤버에 접근하는 속도도 비가상 기본 클래스의 데이터 멤버에 접근하는 속도보다 느리며, 기본 클래스의 초기화에 관련된 규칙도 비가상 기본 클래스의 초기화 규칙보다 훨씬 복잡하고 직관성이 떨어진다.
가상 기본 클래스의 초기화 규칙은 다음과 같은데,
1. 초기화가 필요한 가상 기본 클래스로부터 클래스가 파생된 경우, 이 파생 클래스는 가상 기본 클래스와의 거리에 상관없이 가상 기본 클래스의 존재를 염두에 두고 있어야 한다.
2. 기존의 클래스 계통에 파생 클래스를 새로 추가할 때도 그 파생 클래스는 가상 기본 클래스의 초기화를 떠맡아야 한다.
는 점이다.
결과적으로, 가상 기본 클래스의 비용은 비싸다.
그래서 굳이 쓸 필요가 없다면 가상 기본 클래스를 사용 안 하는 것이 좋으며, 가상 기본 클래스를 정말 쓰지 않으면 안 될 상황이라면 가상 기본 클래스에는 데이터를 넣지 않는 쪽으로 최대한 신경을 써라. 데이터만 들어가지 않으면 가상 기본 클래스의 초기화 규칙에서 해방될 수 있다.
참고로, C++의 가상 기본 클래스와 여러 가지 측면에서 비교가 되는 것이 자바와 닷넷의 Interface인데, 인터페이스는 언어적으로 데이터를 아예 가지지 못하도록 정의되어 있다.
그럼 이 점에 착안해서, C++의 인터페이스를 사용해서 다중 상속을 효과적으로 구현해보면
class IPerson // 용도에 따라 구현될 인터페이스
{
public:
virtual ~IPerson();
virtual std::string name() const = 0;
virtual std::sstring birthDate() const = 0;
};
class DatabaseID {...};
class PersonInfo // IPerson 인터페이스를 구현하는데 유용한 함수들 모음
{
public:
explicit PersonInfo(DatabaseID pid);
virtual ~PersonInfo();
virtual const char* theName() const;
virtual const char* theBirthDate() const;
...
private:
virtual const char* valueDelimOpen() const;
virtual const char* valueDelimClose() const;
...
};
class CPerson:public IPerson, private PersonInfo
{
public:
explicit CPerson(DatabaseID pid): PersonInfo(pid) {}
// IPerson의 순수 가상 함수에 대해 파생 클래스의 구현을 제공
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 "";}
};
이런 식으로 의미 있게 구현할 수 있다.
요약
- 다중 상속은 단일 상속보다 복잡하다. 새로운 모호성 문제를 일으킬 뿐만 아니라 가상 상속이 필요해질 수도 있다.
- 가상 상속을 쓰면 크기 비용, 속도 비용이 늘어나며, 초기화 및 대입 연산의 복잡도가 커진다. 따라서 가상 기본 클래스에는 데이터를 두지 않는 것이 현실적으로 가장 실용적이다.
- 다중 상속을 적법하게 쓸 수 있는 경우가 있다. 여러 시나리오 중 하나는 인터페이스 클래스로부터 public 상속을 시킴과 동시에 구현을 돕는 클래스로부터 private 상속을 시키는 것이다.
'Books > Effective C++' 카테고리의 다른 글
[Effective C++] 42. typename의 두 가지 의미를 제대로 파악하자 (0) | 2022.07.08 |
---|---|
[Effective C++] 41. 템플릿 프로그래밍의 천릿길도 암시적 인터페이스와 컴파일 타입 다형성부터 (0) | 2022.07.08 |
[Effective C++] 39. private 상속은 심사숙고해서 구사하자 (0) | 2022.07.03 |
[Effective C++] 38. "has-a(..는..를 가짐)" 혹은 "is-implemented-in-terms-of(..는..를 써서 구현됨)"를 모형화할 때는 객체 합성을 사용하자 (0) | 2022.07.03 |
[Effective C++] 37. 어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자 (0) | 2022.07.02 |
댓글
이 글 공유하기
다른 글
-
[Effective C++] 42. typename의 두 가지 의미를 제대로 파악하자
[Effective C++] 42. typename의 두 가지 의미를 제대로 파악하자
2022.07.08 -
[Effective C++] 41. 템플릿 프로그래밍의 천릿길도 암시적 인터페이스와 컴파일 타입 다형성부터
[Effective C++] 41. 템플릿 프로그래밍의 천릿길도 암시적 인터페이스와 컴파일 타입 다형성부터
2022.07.08 -
[Effective C++] 39. private 상속은 심사숙고해서 구사하자
[Effective C++] 39. private 상속은 심사숙고해서 구사하자
2022.07.03 -
[Effective C++] 38. "has-a(..는..를 가짐)" 혹은 "is-implemented-in-terms-of(..는..를 써서 구현됨)"를 모형화할 때는 객체 합성을 사용하자
[Effective C++] 38. "has-a(..는..를 가짐)" 혹은 "is-implemented-in-terms-of(..는..를 써서 구현됨)"를 모형화할 때는 객체 합성을 사용하자
2022.07.03