Notice
Recent Posts
Recent Comments
Link
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Archives
Today
Total
관리 메뉴

로또

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

책/Effective C++

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

아롱로또 2023. 12. 9. 18:03

다루는 내용

다중 상속의 문제점을 해결하기 위한 가상 상속에 대해서 알아보고, 가상 상속의 단점에 대해서도 알아보자. 또한 다중 상속이 올바르게 쓰일 수 있는 시나리오에 대해서도 다룬다. 

다중 상속이란?

다중 상속(Multiple Interitance: MI)은 하나의 클래스가 두 개 이상의 기본 클래스를 동시에 상속받는 것을 말한다.

 

다중 상속의 문제점

1. 이름의 모호성

둘 이상의 기본 클래스로부터 똑같은 이름을 물려받을 가능성이 생긴다.

class BorrowableItem{ // 라이브러리에서 가져올 수 있는 아이템
public:
	void CheckOut(); // 라이브러리로부터 체크아웃
};

class ElectronicGadget{
private:
	bool CheckOut() const; // 자체 테스트 함수
};

class MP3Player:
	public BorrowableItem,
	public ElectronicGadget // 다중 상속 발생
{ ... };

MP3Player mp3;
mp3.CheckOut(); // 호출 함수의 모호함

코드를 자세히 보면, BorrowableItem::Checkout()은 public인 반면, ElectronicGadget::CheckOut()은 private에 속해 있어 파생 클래스인 MP3Player가 접근할 수 있는 함수는 BorrowableItem::CheckOut()으로 분명해보인다.

하지만 컴파일러는 C++ 규칙을 사용해 주어진 호출에 대해 최적으로 일치하는(best-match) 함수를 찾은 후에 함수의 접근 가능성을 점검한다. 두 기본 클래스의 CheckOut()은 C++ 규칙에 의한 일치도가 동일하여 최적 일치 함수가 결정되지 않는다. 때문에 함수의 접근 가능성이 점검되지 않는다.

 

2. 죽음의 다중상속 다이아몬드

class File{ ... };
class InputFile: public File { ... };
class OutputFile: public File { ... };
class IOFile:
	public InputFile,
	public OutputFile // 다중 상속 발생
{ ... };

이를 클래스 다이어그램으로 표현하면 다음과 같은 다이아몬드 모양을 보인다. 이를 다중 상속 다이아몬드라고도 한다.

죽음의 다중 상속 다이아몬드

위처럼 기본 클래스와 파생 클래스 간의 경로가 두 개 이상이 되는 상속 계통을 사용하게 되면, 기본 클래스의 데이터 멤버가 경로의 개수만큼 중복 생성되는 모습을 볼 수 있을 것이다.

#include <iostream>
using namespace std;

class File{
public:
	File(){ cout << "File()" << endl; }
private:
	string file_name;
};
class InputFile: public File {
public:
	InputFile(){ cout << "InputFile()" << endl; }
};
class OutputFile: public File {
public:
	OutputFile(){ cout << "OutputFile()" << endl; }
};
class IOFile:
	public InputFile,
	public OutputFile // 다중 상속 발생
{
public:
	IOFile(){ cout << "IOFile()" << endl; }
};

int main(){
	IOFile iof;
	return 0;
}

다중 상속 다이아몬드의 문제점

 

기본 클래스인 File의 생성자가 두 번 호출되는 모습을 볼 수 있다. 

IOFile 클래스는 File 클래스가 가지는 file_name 변수를 몇 개나 가지고 있어야 할까? 바로 위의 기본 클래스 (InputFile, OutputFile) 로부터 사본을 하나씩 물려받으므로 결과적으로 두 개여야 할 것도 같고, IOFile 객체는 파일 이름이 하나만 있는 것이 옳으니 두 기본 클래스로부터 file_name을 동시에 물려받더라도 중복되면 안 될 것도 같다.

 

C++은 두 가지 경우를 모두를 지원하며 기본적으로는 데이터 멤버를 중복생성한다.

만약 중복 생성을 원하지 않는다면 해당 데이터 멤버를 가진 클래스를 가상 기본 클래스(virtual base class)로 만들어 해결할 수 있다. 

 

가상 상속

class InputFile: virtual public File {
public:
	InputFile(){ cout << "InputFile()" << endl; }
};
class OutputFile: virtual public File {
public:
	OutputFile(){ cout << "OutputFile()" << endl; }
};

가상 상속으로 변경

기존 코드에서 virtual 키워드를 사용해 가상 상속으로 변경한 후 실행한 결과이다.

가상 상속

File의 생성자가 한 번만 호출됨을 확인할 수 있다.

 

가상 상속의 단점

가상 상속을 통한 데이터 멤버의 중복생성을 막기 위해 컴파일러는 꼼수를 사용하는데, 이러한 꼼수는 다음과 같이 여러 오버헤드를 가져온다. (Virtual Base Table Pointer 등)

  1. 가상 상속을 사용하는 클래스로 만들어진 객체는 그렇지 않은 객체보다 일반적으로 크기가 크다.
  2. 가상 기본 클래스의 데이터 멤버에 접근하는 속도도 비가상 기본 클래스의 데이터 멤버에 접근하는 속도보다 느리다. 
  3. 가상 기본 클래스의 초기화 규칙은 비가상 기본 클래스 초기화 규칙보다 복잡하고 직관적이지 않다.

때문에 굳이 사용할 필요가 없다면 가상 상속을 피하되, 불가피한 상황이라면 가상 기본 클래스에는 데이터를 넣지 않도록 주의를 기울이자. C++의 가상 기본 클래스와 비교되는 것이 자바와 닷넷의 Interface 개념인데, 해당 언어에서는 데이터를 갖지 못하도록 한다.

 

다중상속의 적합한 사용 예제

간단히 요약하면, 인터페이스 클래스로부터 public 상속을 시킴과 동시에 구현을 돕는 클래스로부터 private 상속을 시키는 시나리오이다.

예제에서는 사람을 모형화할 것이다. 인터페이스 클래스 IPerson으로부터 public 상속한 클래스 CPerson을 구현할 때, CPerson의 구현을 돕는 클래스인 PersonInfo을 private 상속하는 것이다.

 

예제 코드는 다음과 같다.

// 인터페이스 클래스
class IPerson{
public:
	virtual ~IPerson();
	virtual string Name() const = 0;
	virtual string BirthDate() const = 0;
};

// 구현을 돕는 클래스
class PersonInfo{
public:
	virtual ~PersonInfo();
	virtual const char* TheName() const
	{ 
		static char value[MAX_LENGTH]; // 정적 변수, 0으로 초기화
		strcpy(value, ValueDelimOpen()); // value = [
		... // value 에 실제 이름 저장 // value = [Lotto
		strcat(value, ValueDelimClose()); // value = [Lotto]
		return value; // 구분자로 감싸여진 이름 반환
	}
	virtual const char* TheBirthDate() const{...}
private:
	// 기본 지정된 시작 구분자
	virtual const char* ValueDelimOpen() const { return "["; }
	// 기본 지정된 끝 구분자
	virtual const char* ValueDelimClose() const { return "]"; }
};

// 다중 상속하여 구현
class CPerson: public IPerson, private PersonInfo{
public:
	virtual string Name() const
	{ return PersonInfo::TheName(); }
	
	virtual string BirthDate() const
	{ return PersonInfo::TheBirthDate(); }
private:
	// 재정의한 시작 구분자
	virtual const char* ValueDelimOpen() const { return ""; }
	// 재정의한 끝 구분자
	virtual const char* ValueDelimClose() const { return ""; }
};

 

이것만은 잊지 말자!

  • 다중 상속은 단일 상속보다 확실히 복잡하다. 새로운 모호성 문제를 일으킬 뿐만 아니라 가상 상속이 필요해질 수도 있다.
  • 가상 상속을 쓰면 크기 비용, 속도 비용이 늘어나며, 초기화 및 대입 연산의 복잡도가 커진다. 따라서 가상 기본 클래스에서는 데이터를 두지 않는 것이 현실적으로 가장 실용적이다.
  • 다중 상속을 적법하게 쓸 수 있는 경우가 있다. 여러 시나리오 중 하나는 인터페이스 클래스로부터 public 상속을 시킴과 동시에 구현을 돕는 클래스로부터 private 상속을 시키는 것이다.