항목 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 상속의 동작
    1. 컴파일러는 일반적으로 파생클래스 객체를 기본클래스 객체로 반환하지 않음
    2. 기본 클래스에서 물려받은 멤버는 파생 클래스에서 private 멤버가 된다. (public / protected => private)
  • private 상속의 의미 : is implemented in terms of(즉 그 자체로 구현 기법 중의 하나. 구현만 물려받을 수 있고 인터페이스는 물려받을 수 없다.)

  • private 상속 / 객체 합성 중 선택 : 할 수 있으면 객체 합성으로 하되 꼭 해야 한다면 private 상속을 사용하자.
  • 꼭 해야 하는 경우
    1. 비공개(protected) 멤버 접근
    2. 가상함수 재정의
  • 구조 복잡도가 올라가지만 private 상속 대신 객체 합성으로 구현할 때의 장점 ( ex) Parent를 private 상속 받는다 => Parent를 구현한 child를 객체 합성한다. )
    1. 클래스 설계 시 파생은 가능하게 하되 파생 클래스에서 Parent 클래스의 인터페이스를 재정의할 수 없도록 설계차원에서 막고 싶을 때
      • 자바의 final, C#의 sealed를 대체할 아이디어
    2. 컴파일 의존성 최소화
      • include 되는 정의를 줄일 수 있다.
  • 공백 클래스 : 데이터가 전혀 없는 클래스
  • 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) : 일반적으로 단일 상속 하에서만 적용한다.
    1. static 멤버를 제외한(힙과 스택에 할당되는) 멤버 데이터가 존재하지 않아야 한다.
    2. 가상 테이블이 생기므로 virtual 함수가 있으면 안된다.
    3. 가상 테이블이 생기므로 virtual base 클래스가 없어야 한다.
  • 즉 공백 클래스는 다음을 가질 수 있다.
    1. static 멤버 데이터
    2. non-virtual 멤버 함수
    3. typedef 나 enum 같은 것

항목 40 : 다중 상속은 심사숙고해서 사용하자

  • 다중 상속 시 고려해야 할 것
    1. 둘 이상의 기본 클래스로부터 똑같은 이름을 물려받을 가능성(모호성)이 발생한다.
      • 파생 클래스가 접근할 수 있는 함수가 딱 결정되는 게 분명해도 모호성이 발생한다.
      • ex) 부모 클래스1의 멤버는 public, 부모 클래스2의 멤버는 private
      • 어떤 함수가 접근 가능한지 알아보기 전에 c++ 컴파일러가 이 규칙으로 호출에 대해 최적으로 일치하는 함수인지 먼저 확인하자. 호출할 기본 클래스의 함수를 손수 지정하는 방법으로 처리
    2. 기본 클래스와 파생 클래스 사이 경로가 두개 이상 되는 상속 계통을 쓰면 기본 클래스 데이터 멤버가 경로 개수만큼 중복 생성된다.(죽음의 MI 마름모꼴(deadly MI diamond))
      • 데이터 멤버를 중복 생성한 게 아니었다면 가상 기본 클래스로 만든다.
        • 가상 기본 클래스로 만들 클래스에 직접 연결된 파생클래스에서 가상상속한다.
  • 정확한 동작의 관점에서 public 상속은 항상 반드시 가상 상속으로 하자. (그러나.. 비싸다.)
    1. 데이터 멤버 중복 생성을 막기 위한 보이지 않는 컴파일러의 숨은 꼼수가 있기 때문이다.(가상함수 테이블에 쑤셔넣기)
    2. 그래서 일반적으로 가상 상속을 사용하는 클래스가 안 쓸때보다 크기가 크다.
      1. 데이터 멤버 접근 속도도 느리다.
  • 초기화 규칙이 훨씬 복잡하고 직관성도 떨어진다. >1. 초기화가 필요한 가상 기본 클래스로부터 클래스가 파생된 경우 이 파생 클래스는 가상 기본 클래스와의 거리에 상관 없이 가상 기본 클래스의 존재를 염두에 두고 있어야 한다. >2. 기존 클래스 계통에 파생 클래스를 새로 추가할 때도 그 파생 클래스는 가상 기본 클래스의 초기화를 거리와 상관없이 떠맡아야 한다.

  • 즉 구태여 쓸 필요 없으면 쓰지 말자.
  • 굳이 사용할 때는 가상 기본 클래스에는 데이터를 넣지 말자.
    • 가상 기본 클래스의 초기화 규칙으로부터 해방된다.
  • 다중 상속을 적법하게 쓰는 경우 중 하나는 public 상속과 private 상속을 함께 사용할 때이다.