[Effective C++] 항목 38 ~ 40
항목 38 : “has a(~는 ~를 가짐)” 혹은 “is implemented in terms of(~는 ~를 써서 구현됨)”를 모형화할 때는 객체 합성을 사용하자.
- 합성 : 어떤 타입 객체들이 그와 다른 타입의 객체들을 포함하고 있을 경우 성립하는 그 타입들 사이의 관계
- 합성, 레이어링, 포함, 통합, 내장 등으로 동일한 용어 뜻으로 사용된다.
- 상속의 의미 : is a
- 합성의 의미 : has a, is implemented in terms of
- 응용 영역(일상 생활 사물을 본뜬 것) : has a
- 구현 영역(시스템 구현만을 위한 인공물) : is implemented in terms of
- ex) list를 사용하여 Set 클래스를 구현한다(is implemented in terms of)
항목 39 : private 상속은 심사숙고해서 구사하자
- private 상속의 동작
- 컴파일러는 일반적으로 파생클래스 객체를 기본클래스 객체로 반환하지 않음
- 기본 클래스에서 물려받은 멤버는 파생 클래스에서 private 멤버가 된다. (public / protected => private)
-
private 상속의 의미 : is implemented in terms of(즉 그 자체로 구현 기법 중의 하나. 구현만 물려받을 수 있고 인터페이스는 물려받을 수 없다.)
- private 상속 / 객체 합성 중 선택 : 할 수 있으면 객체 합성으로 하되 꼭 해야 한다면 private 상속을 사용하자.
- 꼭 해야 하는 경우
- 비공개(protected) 멤버 접근
- 가상함수 재정의
- 구조 복잡도가 올라가지만 private 상속 대신 객체 합성으로 구현할 때의 장점 ( ex) Parent를 private 상속 받는다 => Parent를 구현한 child를 객체 합성한다. )
- 클래스 설계 시 파생은 가능하게 하되 파생 클래스에서 Parent 클래스의 인터페이스를 재정의할 수 없도록 설계차원에서 막고 싶을 때
- 자바의 final, C#의 sealed를 대체할 아이디어
- 컴파일 의존성 최소화
- include 되는 정의를 줄일 수 있다.
- 클래스 설계 시 파생은 가능하게 하되 파생 클래스에서 Parent 클래스의 인터페이스를 재정의할 수 없도록 설계차원에서 막고 싶을 때
- 공백 클래스 : 데이터가 전혀 없는 클래스
- ex) 만에 하나!
cpp class Empty {}; class HoldsAnInt { private: int x; Empty e; };
cpp- 이 상황일 때 sizeof(HoldsAnInt) > sizeof(int)이다.
- C++ 제약 : 크기가 0인 독립구조 객체가 생기는 것을 금지한다.
- 컴파일러가 이를 지키기 위해 char 한개를 끼우는 식으로 처리한다.
- 하지만 이 제약은 파생 클래스 객체의 기본 클래스에는 적용되지 않는다.
- ex) 수정
class HoldsAnInt : private Empty { private: int x; };
- sizeof(HoldsAnInt) == sizeof(int)
- 공백 기본 클래스 최적화(empty base optimization : EBO) : 일반적으로 단일 상속 하에서만 적용한다.
- static 멤버를 제외한(힙과 스택에 할당되는) 멤버 데이터가 존재하지 않아야 한다.
- 가상 테이블이 생기므로 virtual 함수가 있으면 안된다.
- 가상 테이블이 생기므로 virtual base 클래스가 없어야 한다.
- 즉 공백 클래스는 다음을 가질 수 있다.
- static 멤버 데이터
- non-virtual 멤버 함수
- typedef 나 enum 같은 것
항목 40 : 다중 상속은 심사숙고해서 사용하자
- 다중 상속 시 고려해야 할 것
- 둘 이상의 기본 클래스로부터 똑같은 이름을 물려받을 가능성(모호성)이 발생한다.
- 파생 클래스가 접근할 수 있는 함수가 딱 결정되는 게 분명해도 모호성이 발생한다.
- ex) 부모 클래스1의 멤버는 public, 부모 클래스2의 멤버는 private
- 어떤 함수가 접근 가능한지 알아보기 전에 c++ 컴파일러가 이 규칙으로 호출에 대해 최적으로 일치하는 함수인지 먼저 확인하자. 호출할 기본 클래스의 함수를 손수 지정하는 방법으로 처리
- 기본 클래스와 파생 클래스 사이 경로가 두개 이상 되는 상속 계통을 쓰면 기본 클래스 데이터 멤버가 경로 개수만큼 중복 생성된다.(죽음의 MI 마름모꼴(deadly MI diamond))
- 데이터 멤버를 중복 생성한 게 아니었다면 가상 기본 클래스로 만든다.
- 가상 기본 클래스로 만들 클래스에 직접 연결된 파생클래스에서 가상상속한다.
- 데이터 멤버를 중복 생성한 게 아니었다면 가상 기본 클래스로 만든다.
- 둘 이상의 기본 클래스로부터 똑같은 이름을 물려받을 가능성(모호성)이 발생한다.
- 정확한 동작의 관점에서 public 상속은 항상 반드시 가상 상속으로 하자. (그러나.. 비싸다.)
- 데이터 멤버 중복 생성을 막기 위한 보이지 않는 컴파일러의 숨은 꼼수가 있기 때문이다.(가상함수 테이블에 쑤셔넣기)
- 그래서 일반적으로 가상 상속을 사용하는 클래스가 안 쓸때보다 크기가 크다.
- 데이터 멤버 접근 속도도 느리다.
-
초기화 규칙이 훨씬 복잡하고 직관성도 떨어진다. >1. 초기화가 필요한 가상 기본 클래스로부터 클래스가 파생된 경우 이 파생 클래스는 가상 기본 클래스와의 거리에 상관 없이 가상 기본 클래스의 존재를 염두에 두고 있어야 한다. >2. 기존 클래스 계통에 파생 클래스를 새로 추가할 때도 그 파생 클래스는 가상 기본 클래스의 초기화를 거리와 상관없이 떠맡아야 한다.
- 즉 구태여 쓸 필요 없으면 쓰지 말자.
- 굳이 사용할 때는 가상 기본 클래스에는 데이터를 넣지 말자.
- 가상 기본 클래스의 초기화 규칙으로부터 해방된다.
- 다중 상속을 적법하게 쓰는 경우 중 하나는 public 상속과 private 상속을 함께 사용할 때이다.