#include<cstdio>
class Person { };
class Student : private Person{ };
void eat(const Person& p) {}
void study(const Student& s) {}
int main() {
Person p;
Student s;
eat(p);
study(s);
// eat(s); // 에러!
}
private 상속
은 public 상속
과 대조적으로 파생 객체(자식객체)가 기본 클래스 객체(부모객체)로의 변환이 불가능하다.private상속
을 받은 객체의 기능을 사용할 수 있다.private상속
은 is-a
관계가 아닌 is-implemented-in-terms-of
관계로 보는것이 맞다.private
상속의 의미는 “구현만 물려받을 뿐 동일한 인터페이스는 아니다”라고 생각하면 된다.프로파일링을 위해 내가 사용하는 Widget
클래스의 호출 횟수, 시간에 따른 호출 빈도를 추적하고 싶다고 가정해보자.
마침, Widget
에서 필요로하는 시간 관련 기능이 Timer
클래스에 미리 구현돼있다면?
class Timer {
public:
explicit Timer(int tickFrequency) {}
virtual void onTick() const;
};
Timer
대한 기능들에 접근이 가능하고 내가 원하는대로 동작하게 하기 위한 오버라이딩도 가능하다.class Widget : private Timer {
private:
virtual void onTick() const override;
};
has-a
관계를 표현하는데 더 적합하지 않나?class Widget{
private:
class WidgetTimer : public Timer {
public:
virtual void onTick() const override;
};
WidgetTimer timer;
};
Widget
을 상속받는 파생 클래스에서 onTick()
함수를 오버라이딩 하는 등의 상황을 막을 수 있다. (C++은 C#처럼 sealed
키워드를 지원하지 않기 때문 ->사실 이부분도 C++11에서 override키워드와 함께 final 키워드를 지원하기 시작함)Timer
를 상속받으려면 선언부에서 헤더파일을 include
해야 하기에 컴파일 의존성이 생기지만 컴포지션 패턴을 사용하면 전방선언을 활용하고 선언부가 아닌 구현부에서 include
해도 되기에 컴파일 의존성을 줄일 수 있다.protected
로 선언돼있다면 위에서 했던 것처럼 Timer
클래스를 WidgetTimer
로 상속받은 이후 필요한 정보들을 노출시킬 수 있는 함수를 두면 된다.private
이라면 어차피 Timer
클래스를 Widget
클래스에 private상속
해도 어차피 private
멤버에 대한 접근은 불가능하다.Timer
클래스를 WidgetTimer
로 상속받고 재정의 하면 된다.class Empty {};
class HoldsAnInt {
int x;
Empty e;
};
int main() {
printf("%d byte\n",sizeof(HoldsAnInt));
return 0;
};
8 byte
가 나온다. 4 byte
가 아니다.Empty e;
를 통해 인스턴스를 독립적으로 요구한다면, 이러한 “공백” 객체에 컴파일러는 char
한 개(1 byte
)를 슬그머니 끼어넣는다.HoldsAnInt
클래스를 4 byte
에 맞춰 byte alignment까지 하면 총 5 byte
→ 8 byte
라는 결과가 나오게 된다.class Empty {};
class HoldsAnInt : private Empty {
int x;
};
int main() {
printf("%d byte\n",sizeof(HoldsAnInt));
};
그래서 위와 같이 구현하면 총 4 byte
가 나오게 된다.
위 예제에서는 Empty클래스가 비어있어서 아무 의미도 없어 보이지만, 사실 공백클래스는 진짜 비어있는 클래스는 아니다.
private 상속
을 정당화 할 수 없다는 것이다.private 상속
이 구현되지 않은것만 봐도 답이 나온다.