언어/C++

[Effective C++] 다중 상속은 심사숙고해서 사용하자

지나가던 개발자 2020. 2. 9. 12:19
반응형

항목 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 상속은 반드시 항상 가상 상속이어야 하는 것이 맞습니다.

상속되는 데이터 멤버의 중복생성을 막는 데는 우리 눈에는 보이지 않는 컴파일러의 숨은 꼼수가 필요합니다.

그리고 그 꼼수 덕택에, 가상 상속을 사용하는 클래스로 만들어진 객체는 가상 상속을 쓰지 않은 것보다 일반적으로 크기가 더 큽니다. 게다가 가상 기본 클래스의 데이터 멤버에 접근하는 속도도 비가상 기본 클래스의 데이터 멤버에 접근하는 속도보다 느립니다. 세부적인 크기, 속도 차이는 컴파일러마다 다르지만, 이 점만은 분명히 말할 수 있습니다. 가상 상속은 비쌉니다.

 

비용지불이 이것으로 끝나는게 아닙니다.

가상 기본 클래스의 초기화에 관련된 규칙은 비가상 기본 클래스의 초기화 규칙보다 훨씬 복잡한데다가 직관성도 더 떨어집니다. 대부분의 경우, 가상 상속이 되어 있는 클래스 계통에서는 파생 클래스들로 인해 가상 기본 클래스 부분을 초기화할 일이 생기게 됩니다. 이때 들어가는 초기화 규칙은 다음과 같습니다.

  1. 초기화가 필요한 가상 기본 클래스로부터 클래스가 파생된 경우, 이 파생 클래스는 가상 기본 클래스와의 거리에 상관없이 가상 기본 클래스의 존재를 염두에 두고 있어야 합니다.
  2. 기존의 클래스 계통에 파생 클래스를 새로 추가할 때도 그 파생 클래스는 가상 기본클래스의 초기화를 떠맡아야 합니다.

가상 기본 클래스에 대한 조언은 간단합니다.

  1. 구태여 쓸 필요가 없으면 가상 기본 클래스를 사용하지 마세요.
  2. 가상 기본 클래스를 정말 쓰지 않으면 안 될 상황이라면, 가상 기본 클래스에는 데이터를 넣지 않는 쪽으로 최대한 신경을 쓰세요.

다중 상속을 적법하게 쓸 수 있는 경우가 있습니다.

인터페이스 클래스로부터 public 상속을 시킴과 동시에 구현을 돕는 클래스로부터 private 상속을 시키는 것입니다.

 

꼭 잊지 말아야 할 것!

1. 다중 상속은 단일 상속보다 확실히 복잡합니다. 새로운 모호성 문제를 일으킬 뿐만 아니라 가상 상속이 필요해질 수도 있습니다.

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

3. 다중 상속을 적법하게 쓸 수 있는 경우가 있습니다. 여러 시나리오 중 하나는, 인터페이스 클래스로부터 public 상속을 시킴과 동시에 구현을 돕는 클래스로부터 private 상속을 시키는 것입니다.

반응형