스토리텔링 개발자

[Effective C++] 39. private 상속 본문

개발/Effective C++

[Effective C++] 39. private 상속

김디트 2024. 7. 9. 11:06
728x90

항목 39. private 상속은 심사숙고해서 구사하자

 

 

 

private 상속의 동작
  1. public 상속과 달리, 컴파일러는 일반적으로 파생클래스 객체를 기본클래스 객체로 변환하지 않는다.
    • 즉, 기본 클래스를 매개변수로 받는 함수를 파생 클래스 객체로 호출할 수 없다는 의미이다.
  2. 기본 클래스에서 물려받은 멤버는 파생 클래스에서 모조리 private 멤버가 된다.
    • 즉, 기본 클래스의 public,. protected 멤버는 파생 클래스에서 private 멤버가 된다.

 

 

 

private 상속의 의미
  • is implemented in terms of (...는 ...를 써서 구현된다.)
    • 즉 private 상속은 그 자체로 구현 기법 중의 하나라고 할 수 있다.
  • 구현만 물려받을 수 있고 인터페이스는 물려받을 수 없다.
    • 소프트웨어 설계(design) 도중에는 아무런 의미도 갖지 않으며, 단지 소프트웨어 구현(implementation) 중에만 의미를 가진다.

 

 

 

private 상속과 객체 합성 중 어떤 걸 선택할 것인가?
  • private 상속과 객체 합성은 같은 뜻(is implemented in terms of)을 가진다.
  • 할 수 있으면 객체 합성으로 하되 꼭 해야 한다면 private 상속을 사용하자.
    • 아무래도 객체 합성 쪽이 가독성이 좋을 것이다.

 

 

 

private 상속을 꼭 해야 하는 경우
  1. 비공개(protected) 멤버에 접근해야 하는 경우
    • 객체 합성으로는 합성된 객체의 protected 멤버에 접근할 수 없다.
  2. 가상함수를 재정의해야 하는 경우
    • 객체 합성으로는 합성된 객체의 virtual 함수를 재정의하기 까다롭다.
  3. 공간 최적화가 얽힌 '만약의 경우'
    • 공백 기본 클래스 최적화. 아래에서 자세히 알아보기로 한다.

 

 

 

is-implemented-in-terms-of 구현의 두 가지 방법
class Timer
{
public:
    explicit Timer(int tickFrequency);
    virtual void onTick() const;
    ...
};

// private 상속을 활용한 방식
class Widget : private Timer
{
private:
    virtual void onTick() const;
    ...
};

// 객체 합성을 활용한 방식
class Widget
{
private:
    class WidgetTimer : public Timer
    {
    public:
        virtual void onTick() const;
        ...
    };
    
    WidgetTimer timer;
    ...
};

 

 

 

private 상속 대신 객체 합성으로 구현할 때의 장단점
  • 단점
    • 객체 합성으로 구현할 시, private 상속에 비해 구조 복잡도가 올라간다.
  • 장점
    1. Widget 클래스를 설계할 때, 파생 클래스에서 onTick을 재정의할 수 없도록 설계 차원에서 막고 싶을 때 유용하다.
      • 자바의 final, C#의 sealed를 대체할 아이디어라 할 수 있지만, 모던 C++에서는 언어 상으로 final을 지원한다.
    2. 컴파일 의존성을 최소화하고 싶을 때 좋다.
      • WidgetTimer 객체에 대한 포인터를 가지도록 하고, WidgetTimer의 정의를 다른 헤더로 빼는 것만으로도 컴파일 의존성을 줄일 수 있을 것이다.

 

 

 

private 상속을 사용해야 하는, 공간 최적화가 얽힌 만약의 경우
  • 공백 클래스(empty class) (데이터가 전혀 없는 클래스)
    • 비정적 데이터 멤버가 하나도 없어야 한다.
      • static 데이터 멤버는 허용된다.
    • 가상 함수가 하나도 없어야 한다.
      • 가상 함수가 한 개라도 있으면 각 객체마다 vptr이 하나씩 추가되기 때문이다.
    • 가상 기본 클래스도 없어야 한다.
      • 가상 기본 클래스는 크기 오버헤드를 일으키는 요인이다.
  • 이런 공백 클래스는 개념적으로 차지하는 메모리 공간이 없는 것이 맞다.
  • 하지만, C++에는 "독립 구조(freestanding)의 객체는 반드시 크기가 0을 넘어야 한다"는 금기사항이 있다.
// 공백 클래스
class Empty {};

// 공백 클래스 + int 이므로 크기는 sizeof(int)여야 할 것 같지만..
class HoldsAnInt 
{
private:
    int x;
    Empty e;
};
  • 이 경우, sizeof(HoldsAnInt) > sizeof(int) 가 되는 괴현상.
  • 크기가 달라지는 이유
    • 크기가 0인 독립구조 객체가 생기는 것을 금지한다는 제약을 지키기 위함.
      • 대부분의 컴파일러는 char 한개를 끼우는 식으로 처리한다.
    • 바이트 정렬(alignment)(항목 50 참조)이 필요해지는 경우가 생긴다.
      • 바이트 패딩(padding) 과정이 추가되면서, char 하나의 크기를 넘기게 된다.
      • 보통은 int 크기 정도로 늘어난다.
  • 하지만 이 C++ 제약(크기가 0인 독립구조 객체가 생기는 것을 금지한다.)은 파생 클래스 객체의 기본 클래스 부분에는 적용되지 않는다.
    • 기본 클래스 부분은 독립구조 객체가 아니기 때문이다.
  • 그렇다면 이렇게 구현하면?
class HoldsAnInt : private Empty
{
private:
    int x;
};
  • 이 경우, sizeof(HoldsAnInt) == sizeof(int) 를 만족하게 된다.

 

 

 

공백 기본 클래스 최적화(empty base optimization : EBO)
  • private 상속을 활용하여 공백 클래스의 크기를 최적화한다.
  • 일반적으로 단일 상속 하에서만 적용한다.
  • 사실 공백 클래스는 진짜로 텅 빈 것이 아니다.
    • 비정적 데이터는 없지만...
    • typedef, enum, 정적 데이터 멤버, 비가상 함수 등을 가질 수 있다.
  • STL에서는 기술적으로 공백 처리된 클래스가 많이 있다.
    • unary_function
    • binary_function
    • 사용자 정의 함수 객체를 만들 때 상속시킬 기본 클래스로 굉장히 자주 사용되는 클래스들이다.
    • 공백 클래스들이므로, 공백 기본 클래스 최적화가 적용된다.
728x90
Comments