- private 상속의 의미는 is-implemented-in-terms-of이다. 파생 클래스 쪽에서 기본 클래스의 protected 멤버에 접근해야 할 경우 혹은 상속받은 가상 함수를 재정의해야 할 경우에 private 상속이 의미가 있다.
- 객체 합성과 달리, private 상속은 공백 기본 클래스 최적화(EBO)를 활성화시킬 수 있다.
private 상속
public 상속은 is-a 관계로 나타낸다.
하지만, private 상속은 is-a 관계로 나타내지 않는다.
class Person { ... };
class Student: private Person { ... };
void eat(const Person& p);
void study(const Student& s);
Person p;
Studnet s;
eat(p); // OK
eat(s); // Error
private 상속의 첫 번째 규칙은 컴파일러가 암시적으로 기본 객체로 변환하지 않는다는 것이다.
그렇기 때문에 eat 함수 호출이 s에 대해서 실패한 것이다.
두 번째 규칙은 기본 클래스로부터 물려받은 멤버는 파생 클래스에서 모두 private 멤버가 된다는 것이다.
private 상속의 사용
private 상속의 의미는 is-implemented-in-terms-of이다.
B클래스로부터 private 상속을 통해 D클래스를 파생시킨 것은 B클래스에서 쓸 수 있는 기능들 몇 개를 활용할 목적이지, B 타입과 D타입의 객체 사이에 어떤 개념적 관계가 있어서 하는 행동이 아니다.
즉, private 상속은 구현 기법 중 하나라고 생각하면 된다.
private 상속의 의미는 구현만 물려받는 것이고 인터페이스는 물려받지 않는다.
private 상속과 객체 합성 중 어느 것을 사용해야 할지 헷갈릴 수 있다.
정답은 되도록이면 객체 합성을 사용하고, 꼭 필요한 경우라면 private 상속을 사용하는 것이다.
꼭 필요한 경우는 비공개 멤버를 접근할 때 혹은 가상 함수를 재정의할 경우를 의미한다.
또한, 공간 문제가 있을 때도 private 상속을 사용해야 한다.
예를 들어, Widget 객체를 사용하는 응용프로그램을 하나 만들고 있다고 가정해 보자.
Widget의 멤버 함수의 호출 횟수 같은 것들을 알고 싶고 실행 시간이 지남에 따라 호출 비율이 어떻게 변하는 지 보고 싶다.
각 멤버 함수가 호출되는 횟수를 추적하기 위해 Widget 클래스를 직접 수정하려 한다.
Timer를 설치하여 통계 자료에 활용할 예정이다.
class Timer {
public:
explicit Timer(int tickFrequency);
virtual void onTick() const;
...
};
Timer 객체는 반복적으로 시간을 경과시킬 주기를 정할 수 있고, 일정 시간이 경과할 때마다 가상 함수를 호출하도록 되어 있다. 이 가상 함수를 재정의해서 Widget 객체의 상태를 점검하면 된다.
그러려면 Widget 클래스에서 Timer의 가상 함수를 재정의할 수 있어야 하므로 Widget 클래스는 Timer를 상속받아야 한다.
하지만 이런 상황에서는 public 상속은 맞지 않다. Widget은 Timer의 일종도 아니고 onTick 같은 함수의 인터페이스를 사용자에게 넘겨주면 안 되기 때문이다.
그렇기 때문에 private 상속을 하는 것이다.
class Widget: private Timer {
private:
virtual void onTick() const;
...
};
private 상속을 했기 때문에, Timer의 public 멤버인 onTick 함수는 Widget에서 private 멤버가 된다.
하지만 onTick 함수를 public 인터페이스로 빼놓게 되면 문제가 발생한다.
사용자가 이 함수를 호출할 수 있다고 생각하기 때문이다.
그렇다면 private 상속 말고 객체 합성으로 해결할 수 있는지 점검해야 한다.
Timer로부터 public 상속을 받은 클래스를 Widget 안에 private 중첩 클래스로 선언해 놓고, 이 클래스에서 onTick을 재정의한 다음, Widget의 데이터 멤버로 갖고 있으면 된다.
class Widget {
private:
class WidgetTimer: public Timer {
public:
virtual void onTick() const;
...
};
WidgetTimer timer;
...
};
private 상속만 써서 만든 설계보다는 복잡한 구조이다.
public 상속과 객체 합성을 모두 사용하고 있고, 클래스를 새로 만들기까지 해야 한다.
하지만, 설계 문제에 대한 접근 방법이 꼭 하나만 있는 것은 아니라는 사실을 보여주는 예시이다.
현실적으로는 private 상속 대신 public 상속과 객체 합성 조합이 더 자주 사용된다.
그러한 이유는 다음과 같다.
- Widget 클래스를 설계하는 데 있어서 파생은 가능하게 하되, 파생 클래스에서 onTick을 재정의할 수 없도록 설계 차원에서 막을 수 있다. 만약 Widget을 Timer로부터 상속시킨 구조라면 이러한 것은 불가능하다. 파생 클래스는 호출할 권한이 없어도 가상 함수를 재정의할 수 있기 때문이다.
Timer를 상속받은 WidgetTimer가 Widget 클래스의 private 영역에 있으면, Widget의 파생 클래스는 WidgetTimer에 접근할 수 없기 때문에 재정의가 불가능하다. - Widget의 컴파일 의존성을 최소화할 수 있다. Widget이 Timer에서 파생된 상태라면, Widget이 컴파일될 때 Timer의 정의 부분이 필요하기 때문에 Timer.h를 #include 해야 할 수 있다.
반면, WidgetTimer의 정의를 Widget으로부터 빼내고 Widget이 WidgetTimer의 객체에 대한 포인터만 갖도록 만들면 WidgetTimer 클래스를 간단히 선언하는 것만으로도 컴파일 의존성을 피할 수 있다.
private 상속을 해야 할 경우는 기본 클래스의 비공개 부분에 파생 클래스가 접근해야 한다거나 가상 함수를 한 개 이상 재정의해야 할 경우가 주된 용도이다.
특히, 공간 최적화를 해야 하는 상황이라면 활용 가치가 크다.
하지만, 이러한 경우는 데이터가 전혀 없는 클래스를 사용하는 경우에만 사용 가능하다.
데이터가 없는 클래스란 비정적 데이터 멤버가 없는 클래스를 말한다.
가상 함수도 하나도 없어야 하고 가상 기본 클래스도 없어야 한다.
이런 공백 클래스는 개념적으로 차지하는 메모리 공간이 없는 게 맞다. 저장할 데이터가 없기 때문이다.
하지만 독립 구조의 객체는 반드시 크기가 0을 넘어야 한다.
class Empty {};
class HoldsAnInt {
private:
int x;
Empty e;
};
sizeof(HoldsAnInt) > sizeof(int)가 참이 된다
Empty는 데이터가 없어 크기가 0인데 int와 HlodsAnInt의 크기가 달라지는 이상한 현상이 발생한다.
Empty 타입의 데이터 멤버가 메모리를 요구하기 때문에 발생하는 현상이다. 보통 1과 같은 작은 크기의 값으로 나온다.
(컴파일러가 바이트 정렬이 필요하여 바이트 패딩과정을 추가해 더 커질 수 있다.)
이는 독립 구조 객체의 크기가 0인 것을 방지하기 위함이다.
하지만, 독립구조에만 적용되는 이야기이다. 파생 클래스 객체의 기본 클래스에는 적용되지 않는다.
Empty 타입의 객체를 데이터 멤버로 두지 말고 Empty로부터 상속을 시키면 메모리 크기는 같아진다.
class HoldsAnInt: private Empty {
private:
int x;
};
이 공간 절약 기법은 공백 기본 클래스 최적화(empty base optimization: EBO)라고 한다.
EBO는 일반적으로 단일 상속에서만 적용된다.
공백 클래스는 비정적 데이터 멤버는 갖고 있지 않지만, typedef 혹은 enum, 정적 데이터 멤버, 비가상 함수까지 갖고 있을 수 있다.
STL에는 이와 비슷한 클래스가 많이 있다.
unary_function, binary_function이 그 예인데, 이들은 사용자 정의 함수 객체를 만들 때 상속시킬 기본 클래스로 자주 사용되는 클래스이다.
하지만 공백 객체가 현실적으로 많이 사용되지는 않는다. EBO 때문에 private 상속을 하는 것은 효과적이지 않을 수 있다.
private 상속이 적법한 설계 전략일 가능성이 가장 높은 경우는 두 클래스가 is-a 관계로 이어지지는 않지만 한쪽 클래스가 다른 쪽 클래스의 protected 멤버에 접근해야 하거나 가상 함수를 재정의해야 할 때이다.
하지만 이러한 경우도 private 상속만이 정답은 아니기 때문에 private 상속을 사용할 때는 신중히 결정할 필요가 있다.