[ Effective C++ ] 항목 39 : private 상속은 심사숙고해서 구사하자

Minsu._.Lighting·2023년 12월 11일
0

[ Effective C++ ] 정리 모음집
" C++ 프로그래머의 필독서, 스콧 마이어스의 Effective C++ 를 읽고 내용 요약 / 정리 "

[핵심]

" private 상속은 private 상속 외 방법이 없다면 그 때 사용하자! "

  • private 상속의 의미는 is-implemented-in-terms-of(...는...를 써서 구현됨)이다. 대개 객체 합성과 비교해서 쓰이는 분야가 많지는 않지만, 파생 클래스 쪽에서 기본 클래스의 protected 멤버에 접근해야 할 경우, 상속받은 가상 함수를 재정의해야 할 경우에는 private 상속이 나름대로 의미가 있다!
  • 객체 합성과 달리, private 상속은 공백 기본 클래스 최적화(EBO)를 활성화시킬 수 있다. 이 점은 객체 크기를 가지고 고민하는 라이브러리 개발자에게 꽤 매력적인 특징이 되기도 한다!

💡 private 상속

class Person { ... };

class Student : private Person { ... };

void eat(const Person& p);

void study(const Student& s);

Person p;
Student s;

eat(p);

eat(s);								// Student는 Person의 일종이 아니다...에러!
  • private 상속의 의미는 is-implemented-in-terms-of(...는...를 써서 구현됨) 이다

  • private 상속은 그 자체로 구현 기법 중 하나이다
    - pirvate 상속은 소프트웨어 설계 도중에는 아무런 의미도 갖지 않으며, 구현 중에만 의미를 갖는다.

  • private 상속 관계 시 컴파일러는 파생 클래스 객체를 기본 클래스 객체로 변환하지 않는다

  • 기본 클래스로부터 물려받은 멤버는 파생 클래스에서 private 멤버가 된다
    - 기본 클래스에서 protected 또는 public 이었어도 바뀐다



💡 객체 합성과 private 상속

둘 다 is-implmented-in-terms-of 의미를 갖는다...

  • 객체 합성이 사용 가능하다면 객체 합성을 사용

  • private 합성을 꼭 사용 해야 하는 상황에서는 private 상속을 사용
    - 비공개 멤버를 접근할 때, 가상 함수를 재정의할 때
    - 공백 기본 클래스 최적화 시 사용

[ 예시 코드 ]

class Timer
{
public:
	explicit Timer(int tickFrequency);
    
    virtual void onTick() const;
};

[ Widget 객체를 사용하는 프로그램 제작 중... ]
각 멤버 함수가 몇번 호출 되는지 알기 위해 Timer 클래스를 사용 하기로 했다!

  • Timer 객체는 반복적으로 시간을 경과시킬 주기를 정해줄 수 있다
  • Timer 객체는 일정 시간 경과할 때마다 가상 함수를 호출 하도록 되어있다

📌 private 상속

class Widget : private Timer
{
private:
	virtual void onTick() const;
    ...
};
  • Widget 클래스는 Timer 로부터 private 상속을 받는다
    - Widget 클래스에서 Timer의 가상 함수를 재정의할 수 있어야 하므로
    - public 상속은 is-a 관계여야 하지만 Widget은 Timer의 일종이 아니다

📢 Timer의 public 멤버인 onTick 함수는 Widget에서 private 멤버로 변환된다!
- private 상속을 받았기 때문
- onTick 함수를 public 인터페이스로 빼놓는 순간 사용자는 '이 함수는 호출할 수 있구나'라고 생각하고 실수를 하게 될 것이다!

📌 객체 합성

class Widget
{
private:
	class WidgetTimer : public Timer
    {
    public:
    	virtual void onTick() const;
        ...
    };
    
    WidgetTimer timer;
    ...
};
  • private 상속보다 복잡한 구조를 갖는다
    - public 상속, 객체 합성, 클래스를 새로 만들기까지..

📌 private 상속 대신 public 상속에 객체 합성 조합이 더 자주 쓰이는 이유

  • 클래스를 설계하는 데 있어 파생은 가능하게 하되, 파생 클래스에서 가상 함수를 재정의할 수 없도록 설계 차원에서 막고 싶을 때 유용하다!
    - Widget을 Timer로부터 상속시킨 구조라면 불가능!
    - 상속을 private로 해도 불가능!
    - Timer 로부터 상속을 받은 WidgetTImer가 Widget 클래스의 private 영역에 있으면, Widget 파생 클래스는 WidgetTimer에 접근할 수 없다.

  • 클래스의 컴파일 의존성을 최소화 하고 싶을 때 좋다!
    - Widget이 Timer에서 파생된 상태라면, Widget이 컴파일될 때 Timer의 정의도 끌어올 수 있어야 하기 때문에 Widget의 정의부 파일에서 Timer.h를 #include 해야 할 수도있다



💡 공백 기본 클래스 최적화 시 private 상속의 사용

class Empty { };

class HoldsAnInt
{
private:
	int x;
    Empty e;
};
  • 위 예시 코드에서 sizeof(HoldsAnInt) > sizeof(int)가 되는 괴현상 발생
    - C++에는 "독립 구조의 객체는 반드시 크기가 0을 넘어야 한다"라는 금기사항 같은 것이 정해져 내려오고 있기 때문, 해당 제약 때문에 컴파일러는 "공백" 객체에 char 한개를 끼워 넣는 식으로 처리한다(바이트 정렬이 필요하다 판단할 시 바이트 패딩 과정을 추가해 char 한개(1바이트) 보다 크기가 커질 수 있다)

  • 위 제약은 파생 클래스 객체의 기본 클래스 부분에는 적용되지 않는다

class HoldAnInt : private Empty
{
private:
	int x;
};

- 이 기법을 공백 기본 클래스 최적화(empty base optimizatin: EBO) 라고 알려져있다.



💡 private 상속이 적법한 설계 전략일 가능성이 가장 높은 경우

  • 아무리 봐도 is-a 관계로 이어질 것 같지 않은 두 클래스를 사용할 때
  • 이 둘 사이에서 한쪽 클래스가 다른 쪽 클래스의 protected 멤버에 접근해야 하거나 다른 쪽 클래스의 가상 함수를 재정의해야 할 경우

📢 하지만 private 상속으로만 해결할 수 있는 문제는 아니다!
- 다른 방법도 있기에 private 상속은 섣불리 사용하지 말고 꼭 다른 대안을 고민한 후 사용 하자!

profile
오코완~😤😤

0개의 댓글

관련 채용 정보