본문 바로가기
서적 정리/Effective C++

40.다중 상속은 심사숙고해서 사용하자

by 민돌이2 2021. 12. 22.

학원이든 학교든 다중 상속(multiple inheritance : MI)는 피하라고 배웠다.

둘 이상의 기본 클래스로부터 똑같은 이름을 상속받을 경우 모호성이 발생하기 때문이다.

class A
{
	void Check();
};

class B
{
	bool Check();
};

class C : public A, public B { };

int main()
{
	C c;
	c.Check(); //에러

	system("Pause");
	return 0;
}

c.Check(); 에러

컴파일러는 어떤 함수가 접근 가능한 함수인지 알아보기 전에 호출에 의해 최적으로 일치하는(best-math) 함수인지를 먼저 확인한다. 최적 일치 함수를 찾은 후 함수의 접근가능성을 점검한다는 애기이다.

 

모호성을 해소하려면, 호출할 기본 클래스의 함수를 명시적으로 호출해야한다.

c.A::Check();
c.B::Check();

물론 기본 클래스에서 public 함수일 때만 가능하다.

 

다중 상속의 의미는 둘 이상의 클래스로부터 상속 받는 것이지만, 상위 단계의 기본 클래스를 여러개 갖는 클래스 계통은 소위 죽음의 MI 마름모꼴(deadly MI diamond)라고 알려진 좋지 않은 구조가 나올 수 있다.

class File {};

class InputFile : public File {};
class OutputFile : public File {};

class IOFile : public InputFile, public OutputFile {};

클래스 계층 구조

최상위 클래스인 File 클래스 안에 fileName이라는 데이터 멤버가 있을 때, 최하위 클래스인 IOFile은 fileName 필드가 몇 개가 들어 있을까? InputFile 클래스, OutputFile 클래스 각각 한 개씩 사본을 받아 두 개가 있을 수도 있을 것이고 중복처리하여 한 개만 있을 수 도 있을 것 같다.

C++은 두 가지를 모두 지원하는 애매한 스탠스를 취한다. 기본적으로 데이터 멤버를 중복생성하는 쪽이긴 하다.

만약 데이터 멤버의 중복생성을 원하지 않는다면, 해당 데이터 멤버를 가진 클래스를 가상 기본 클래스(virtual base class)로 만들어 해결할 수 있다. 가상 기본 클래스로 만들 클래스를 파생 클래스에 가상 상속(virtual inheritance)을 사용하게 만드는 것이다.

class File {};

class InputFile : virtual File {}; //virtual 상속
class OutputFile : virtual File {}; //virtual 상속

class IOFile : public InputFile, public OutputFile {};

클래스 계층 구조

이런 MI 상속 계통을 사용하는 표준 C++ 라이브러리가 존재한다. 다만, 클래스가 아니라 클래스 템플릿이라는 차이는 있다. basic_ios, basic_istream, basic_ostream 그리고 basic_iostream이다.

 

사실 정확한 동작의 관점에서 보면, public 상속은 반드시 virtual 상속이어야 한다.

하지만 현실에선 virtual 상속보다 public 상속을 쓴다. 왜 귀찮게 virtual 상속으로 때려박으면 되는데 왜 public 상속을 사용해서 virtaul 상속과 구분지어 놓을까?

virtual 상속은 일반적으로 크기가 더 크고, 가상 기본 클래스의 접근 속도가 느리다. 즉, virtual 상속의 비용은 비싸다.

 

 

virtual 상속

가상 기본 클래스의 초기화 규칙은 비가상 기본 클래스 규칙보다 복잡하고 직관성도 떨어진다.

대부분의 경우 virtual 상속이 되어 있는 클래스 계통에서 파생 클래스들로 인해 가상 기본 클래스 부분을 초기화할 일이 생긴다. 초기화를 할 때 초기화 규칙이 있다.

1.초기화가 필요한 가상 기본 클래스로부터 클래스가 파생된 경우, 이 파생 클래스는 가상 기본 클래스와의 거리에 상관없이 가상 기본 클래스의 존재를 염두에 있어야 한다.

2.기존 클래스 계통에 파생 클래스를 새로 추가할 때도 그 파생 클래스는 가상 기본 클래스의 초기화를 떠맡아야 한다.

규칙이 이해하기도 힘들정도로 짜증난다. 그러므로 쓸 필요가 없으면 쓰지 말고, 가상 기본 클래스가 필요한 상황이라면, 가상 기본 클래스에는 데이터를 최대한 넣지 말자.

 

 

다중 상속은언제 사용해야 할까?

C++ 인터페이스 클래스를 써서 사람을 구현한다고 가정해보자.

class IPerson
{
public:
	virtual ~IPerson();

	virtual string name() const = 0;
	virtual string birthDate() const = 0;
};

class CPerson : public IPerson
{
public:
	virtual string name() const override;
	virtual string birthDate() const override;
};

이런식으로 구현할 것이다. IPerson는 순수 가상 함수가 있으므로 CPerson은 오버라이드를 무조건 해줘야 한다. 직접 구현할 수 있겠지만, 필요한 핵심 기능을 다 갖고 있는 클래스를 발견했다고 가정해보자.

class PersonInfo
{
public:
	explicit PersonInfo(DatabaseID pid);
	virtual ~PersonInfo();

	virtual const char* theName() const
	{
		static char value[255];

		strcpy(value, valueDelimOpen());
		//name을 value에 추가 이를 테면 value + 1부터 해야할 듯 value의 시작은 [이므로
		strcat(value, valueDelimClose());

		return value;
	}

	virtual const char* theBirthDate() const;
	
private:
	virtual const char* valueDelimOpen() const { return "["; } //이름 시작을 표시하기 위한 함수
	virtual const char* valueDelimClose() const { return "]"; } //이름 끝을 표시하기 위한 함수
};

PersonInfo::theName 함수를 사용하면 좋을 것 같다. 하지만 PersonInfo의 [이름] 형식을 갖고 있다. 가상 함수니 재정의해주면 입맛에 맞게 바꿀 수 있다. 이럴 때 다중상속을 사용하면 좋다.

class CPerson : public IPerson, private PersonInfo //다중 상속
{
public:
	explicit CPerson(DatabaseID pid) : PersonInfo(pid) {}

	//IPerson 재정의, PeronInfo 안의 함수 사용
	virtual string name() const override { return PersonInfo::theName(); }
	virtual string birthDate() const override { return PersonInfo::theBirthDate(); }

private:
	virtual const char* valueDelimOpen() const override { return ""; } //PersonInfo 재정의
	virtual const char* valueDelimClose() const override { return ""; } //PersonInfo 재정의
};

클래스 계통 구조

 

 

정리

다중 상속은 단일 상속과 비교해서 사용하기에 복잡하고 이해하기도 복잡하다. 다중 상속과 동등한 효과를 내는 단일 상속 설계가 가능하다면 단일 상속을 선택하는게 정답이다.

다만, 명료하고 유지보수성도 좋은 방법으로 다중 상속일 때가 있다.

 

 

이것만은 잊지 말자

다중 상속은 단일 상속보다 복잡하다. 새로운 모호성 문제를 일으키고 가상 상속이 필요해질 수 있다.

가상 상속을 쓰면 크기 비용, 속도 비용이 늘어나며, 초기화 및 대입 연산의 복잡도가 커진다. 따라서 가상 기본 클래스에는 데이터를 두지 않는 것이 현실적으로 가장 실용적이다.

다중 상속을 적법하게 쓸 수 있는 경우가 있다. 그 중 하나로 인터페이스 클래스로부터 public 상속을 시킴과 동시에 구현을 돕는 클래스로부터 private 상속을 시키는 것이다.

 

 

728x90

댓글