지금까지 내가 설계한 클래스에서 상속을 사용하면 public 상속만 사용했던 것 같다.
상속이라 하면 기본 클래스의 모든 것을 받으니 public 상속으로 대부분 처리가 가능해서 그랬던 것 같다.
이번에는 private 상속으로 해보겠다.
class Person {};
class Student : private Person {};
void eat(const Person& p);
int main()
{
Person p;
Student s;
eat(p); //문제x
eat(s); //에러
system("Pause");
return 0;
}
eat 함수는 Person 클래스를 매개변수로 갖고 있다. public 상속이였다면 Student 객체는 eat 함수의 매개변수로 사용할 수 있었겠지만, private 상속이기에 에러가 발생한다.
private 상속 규칙
1.파생 클래스 객체를 기본 클래스 객체로 변환하지 않는다.
위 코드에서 eat 함수에 Student 객체가 들어갈 수 없었던 이유이다.
2.기본 클래스로부터 물려받은 멤버는 파생 클래스에서 모조리 private 멤버가 된다.
기본 클래스에서 protected 멤버, public 멤버 상관없이 private 멤버가 된다.
private 상속의 의미
is-implemented-in-terms-of(...는 ...을 써서 구현됨)의 의미를 갖는다.
기본 클래스에서 쓸 수 있는 기능들 몇 개를 활용할 목적으로 사용하지, 객체 사이에 개념적 관계는 없다.
private 상속은 그 자체로 구현 기법 중 하나라고 봐도 괜찮다. 기본 클래스는 그저 구현 세부사항이기에 기본 클래스로 부터 물려 받는 것들이 전부 파생 클래스에서 private 멤버가 되는 이유다.
구현만 물려받을 수 있고, 인터페이스는 물려 받을 수 없다.
간단하게 파생 객체가 기본 객체를 써서 구현된다고 생각하면 된다.
객체 합성과의 차이
private 상속과 객체 합성 둘다 is-implemented-in-terms-of(...는 ...을 써서 구현됨)라는 의미를 갖기에 언제 private 상속을 써야하고 객체 합성을 써야 할지 헷갈리는 부분이 있다.
비공개 멤버를 접근해야 할 때, 가상 함수를 오버라이드해야 할 때, 공간 최적화 문제에 얽힐때 private 상속을 하면 된다.
하지만 제목처럼 private 상속은 심사숙고 해야한다.
가상 클래스를 상속하는 파생 클래스를 구현한다고 가정해보자.
private 상속으로 구현한다면
class Timer
{
public:
virtual void onTick() const;
};
class Widget : private Timer
{
private:
virtual void onTick() const override;
};
private 상속하였기 때문에 기본 클래스인 Timer의 public 멤버 가상 함수인 onTick은 파생 클래스인 Widget에서 private 멤버 함수가 되었다. 하지만 onTick을 파생 클래스에서 public 멤버로 놓는다면 어떻게 될까?
컴파일도 사용도 문제는 없지만, private 상속한 의미는 잃을 것이다.
private 상속이 아닌 객체 합성으로 구현한다면
class Timer
{
public:
virtual void onTick() const;
};
class Widget
{
private:
class WidgetTimer : public Timer //public 상속
{
public:
virtual void onTick() const override;
};
private:
WidgetTimer timer; //객체 합성
};
기본 클래스를 public 상속할 클래스를 private 중첩 클래스로 선언 한다(WidgetTimer 클래스). 중첩 클래스에서 가상 함수를 재정의한 다음, 이 클래스의 객체를 데이터 멤버로써 사용한다.
public 상속과 객체 합성을 섞는게 private 상속보다 복잡해 보이긴 하지만 장점이 존재한다.
public 상속 + 객체 합성의 장점
1.파생은 가능하되, 파생 클래스에서 기본 클래스의 가상 함수를 오버라이드 할수 없게 할 수 있다.
만약 Timer 클래스를 상속시킨 구조라면 절대 불가능한 영역이다.
위 코드를 보면 Widget에서는 Timer의 오버라이드는 불가능하다. 그러므로 Widget의 파생 클래스 역시 오버라이드는 불가능하다. 자바의 final 메서드 및 C#의 sealed 메서드가 이 기능을 맡고있다.
2.컴파일 의존성을 최소화할 수 있다.
상속관계인 경우 파생 클래스가 컴파일 될 때 기본 클래스 역시 컴파일 될 수 있다.
반면, 위 코드에서 WidgetTimer의 정의를 Widget으로부터 분리시키면, Widget을 수정하고 컴파일할 때 Timer 클래스는 재 컴파일할 필요가 없다.
공간 최적화
데이터가 전혀 없는 클래스를 사용할 일은 없지만 있다고 가정해보자. 이런 깡통 클래스를 공백 클래스(empty class)라고 한다. 아무 데이터도 없으니 클래스의 크기는 0일 것이라 생각할법 하지만 실제로 C++에는 독립 구조(freestanding)의 객체는 반드시 크기가 0을 넘어야 한다는 법칙이 있어 대부분 컴파일에서 1의 크기를 갖는다.
만약 클래스에 멤버 변수로 int 자료형과 공백 클래스 하나 씩 갖고 있는 클래스의 메모리는 얼마를 차지할까?
class Empty {}; //공백 클래스
class HoldsAnInt
{
int x; //4byte
Empty e; //공백 클래스
};
int main()
{
cout << "Empty size : " << sizeof(Empty) << endl;
cout << "HoldsAnInt size : " << sizeof(HoldsAnInt) << endl;
system("Pause");
return 0;
}
공백 클래스인 Empty의 크기는 1이 출력되고, HoldsAnInt 클래스는 8이 찍힌다.
공백 클래스는 C++의 법칙으로 인해 1인건 알겠는데, HoldsAnInt는 왜 4+1=5인데 왜 8이 찍힐까?
컴파일러는 바이트 정렬(alignment)(50장 참고)이 필요하다 판단되면 바이트 패딩 과정을 추가한다. 바이트 패딩 과정을 거치면서 int 크기 만큼 늘어난다. double로 테스트 해봤는데 16byte가 찍힌다. 나는 지금까지 4바이트 기준으로 패딩처리하는 줄 알았다. 쉐이더에서는 그렇게 하기 때문이다. 바이트 패딩은 좀 더 알아보자.
실제로 사용하는 크기는 int 하나로 4byte인데 8byte를 차지하는게 불합리하게 느껴진다.
이때 해결 방법으로 상속을 사용할 수 있다. 공백 기본 클래스 최적화(empty base optimization : EBO)라고 한다.
class Empty {};
class HoldsAnInt : private Empty
{
int x;
};
int main()
{
cout << "Empty size : " << sizeof(Empty) << endl;
cout << "HoldsAnInt size : " << sizeof(HoldsAnInt) << endl;
system("Pause");
return 0;
}
이것만은 잊지 말자
private 상속의 의미는 is-implemented-in-terms-of(...는 ...을 써서 구현됨)이다. 대개 객체 합성과 비교해서 쓰이는 분야가 많지는 않지만, 파생 클래스 쪽에서 기본 클래스의 protected 멤버에 접근해야 할 경우 혹은 상속받은 가상 함수를 재정의해야 할 경우에는 private 상속이 나름대로 의미가 있다.
객체 합성과 달리, private 상속은 공백 기본 클래스 최적화(EBO)를 활성화시킬 수 있다. 이 점은 객체 크기를 갖고 고민하는 라이브러리 개발자에게 매력적인 특징이다.
'서적 정리 > Effective C++' 카테고리의 다른 글
43.템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자 (0) | 2021.12.23 |
---|---|
42.typename의 두 가지 의미를 제대로 파악하자 (0) | 2021.12.23 |
41.템플릿 프로그래밍의 천릿길도 암시적 인터페이스와 컴파일 타임 다형성부터 (0) | 2021.12.22 |
40.다중 상속은 심사숙고해서 사용하자 (0) | 2021.12.22 |
38."has-a(...는 ...를 가짐)" 혹은 "is-implemented-in-terms-of(...는 ...를 써서 구현됨)"를 모형화할 때는 객체 합성을 사용하자 (0) | 2021.12.22 |
37.어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자 (0) | 2021.12.22 |
36.상속받은 비가상 함수를 파생 클래스에서 재정의하는 것은 절대 금물! (0) | 2021.12.22 |
35.가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러 두자 (0) | 2021.12.21 |
댓글