[Effective C++] 39. private 상속은 심사숙고해서 구사하자
private 상속
public 상속은 is-a관계로 나타낸다.
이 관계는 Student가 Person으로부터 public 상속으로 파생된 상태의 클래스 계통이 주어지면 함수 호출을 성공시키기 위해 컴파일러가 Student를 Person으로 암시적 변환을 수행하는 예를 통해 잘 설명이 된다.(항목 32 참고)
그럼 private 상속은 뭘 의미하는 것일까?
class Person {...};
class Student: private Person {...}; // private 상속
void eat(const Person& p);
void study(const Student& s);
Person p;
Student s;
eat(p); // OK
eat(s); // Error, Student는 Person의 일종이 아님
위의 예시를 봤을 때, private 상속은 is-a를 뜻하지 않음을 알 수 있다.
private 상속은 두 가지 동작 규칙을 가지고 있는데,
1. public 상속과 대조적으로 클래스 사이의 상속 관계가 private이면 컴파일러는 일반적으로 파생 클래스 객체를 기본 클래스 객체로 변환하지 않는다.
2. 기본 클래스로부터 물려받은 멤버는 protected, public에 상관없이 파생 클래스에서 모조리 private 멤버가 된다.
이다.
private 상속이 의미하는 것은 is-implemented-in-terms-of(.. 는.. 를 써서 구현됨)이다. 기본 클래스로부터 private 상속을 통해 파생 클래스를 만든다는 것은, 기본 클래스에서 쓸 수 있는 기능들 몇 개를 활용할 목적으로 한 행동이지 기본 클래스와 파생 클래스 객체 사이에 어떤 개념적 관계가 있어서 한 행동이 아니라는 것이다.
즉, private 상속을 받으면 파생 클래스가 기본 클래스를 써서 구현되는 거라고 생각하면 된다.
private 상속은 소프트웨어 설계 도중에는 아무런 의미도 갖지 않으며, 소프트웨어 구현 중에만 의미를 가진다.
객체 합성과 private 상속 선택 시 고려할 점
구현 할 때, 객체 합성과 private 상속이 같은 의미를 가지므로 어떤 것을 골라야 할지 고민될 수 있다.
결과적으로, 할수 있으면 되도록 객체 합성을 사용하고 꼭 해야 하면 private 상속을 사용하면 된다.
여기서 꼭 private 상속을 해야 하는 경우는 비공개 멤버를 접근할 때 혹은 가상 함수를 재정의 할 경우가 주로 이 경우에 속한다.
예시로 Widget객체에서 타이머를 구현하는 경우 어떤 방법을 선택해야 하는지 생각해보자.
private 상속을 통한 구현
class Widget:private Timer
{
private:
virtual void onTick() const; // Widget 사용 자료 등을 수집한다.
};
원하는 기능을 구현 했지만 onTick함수를 public 인터페이스로 빼놓는 순간 항목 18을 위반하는 것이 된다.
객체 합성을 통한 구현
class Widget
{
private:
class WidgetTimer:public Timer
{
public:
virtual void onTick() const;
...
};
WidgetTimer timer;
...
};
private 상속만 써서 만든 설계와 비교하면 복잡한 구조지만, 다음과 같은 장점 2가지가 있다.
1. Widget 클래스를 설계하는 데 있어서 파생은 가능하게 하되, 파생 클래스에서 onTick를 재정의 할 수 없도록 설계 차원에서 막을 수 있다.
2. Widget의 컴파일 의존성을 최소화하고 싶을 때 좋다. Widget이 Timer에서 파생된 상태라면 Widget이 컴파일 될 때 Timer의 정의도 끌어올 수 있어야 하지만 위와 같은 설계는 WidgetTimer 클래스를 선언하는 것으로 인해 컴파일 의존성을 피할 수 있다.
객체 합성보다 private 상속을 써야 하는 경우도 있는데 이렇게 강제되는 경우는 데이터가 전혀 없는 클래스를 사용할 때이다.
데이터가 없는 클래스란 비정적 데이터 멤버가 없는 클래스를 말한다. 가상 함수도 없어야 하고, 가상 기본 클래스도 없어야한다. 이런 공백 클래스는 개념적으로 차지하는 메모리 공간이 없는 게 맞지만 이런저런 이유로 C++에서는 독립 구조의 객체는 반드시 크기가 0을 넘어야 한다는 규칙이 있어서 메모리 요구가 되는 경우가 있다.
아래의 예시를 봐보자.
class Empty {}; // Empty
class HoldsAnInt
{
private:
int x;
Empty e;
};
sizeof(HoldsAnInt) > sizeof(int)가 된다. 이는 Empty타입의 객체가 C++의 제약을 지키기 위해, 컴파일러는 이런 공백 객체에 char 한개를 넣어 처리하는 식으로 동작하기 때문이다. 하지만 바이트 정렬이 필요하다고 판단되면 컴파일러는 HoldsAnInt 등의 클래스에 바이트 패딩 과정을 추가할 수도 있어서 HoldsAnInt의 크기는 char 하나의 크기를 넘게 된다.
하지만 이런 C++의 제약은 파생 클래스 객체의 기본 클래스 부분에는 적용되지 않기 때문에 Empty 타입의 객체를 데이터 멤버로 두지 않고 상속을 시켜보면,
class HoldsAnInt:private Empty
{
private:
int x;
};
sizeof(HoldsAnInt) == sizeof(int)가 되는 것을 알 수 있다.
이러한 기법은 공백 기본 클래스 최적화(empty base optimization: EBO)라고 알려져 있으며, 모든 컴파일러에서 구현하고 있다.
결과적으로 private 상속이 적법한 전력일 가능성이 높은 경우는, is-a관계로 이어질 것 같지 않은 두 클래스에서 한쪽 클래스가 다른 쪽 클래스의 protected 멤버에 접근해야 하거나 다른 쪽 클래스의 가상 함수를 재정의해야 할 때가 이 경우이다.
하지만 private 상속 아니면 안 되는 것이 아니므로 충분히 고민한 후에 주어진 상황에서 두 클래스 사이의 관계를 나타낼 가장 좋은 방법이 private 상속이라는 결론이 나면 쓰면 된다.
요약
- private 상속의 의미는 is-implemented-in-terms-of(.. 는.. 를 써서 구현됨)이다. 대개 객체 합성과 비교해서 쓰이는 분야가 많지는 않지만, 파생 클래스 쪽에서 기본 클래스의 protected멤버에 접근해야 할 경우 혹은 상속받은 가상 함수를 재정의 할 경우에는 private 상속이 나름대로 의미가 있다.
- 객체 합성과 달리, private 상속은 공백 기본 클래스 최적화를 활성화시킬 수 있다. 이 점은 객체 크기를 가지고 있는 라이브러리 개발자에게 꽤 매력적인 특징이다.
'Books > Effective C++' 카테고리의 다른 글
댓글
이 글 공유하기
다른 글
-
[Effective C++] 41. 템플릿 프로그래밍의 천릿길도 암시적 인터페이스와 컴파일 타입 다형성부터
[Effective C++] 41. 템플릿 프로그래밍의 천릿길도 암시적 인터페이스와 컴파일 타입 다형성부터
2022.07.08 -
[Effective C++] 40. 다중 상속은 심사숙고해서 사용하자
[Effective C++] 40. 다중 상속은 심사숙고해서 사용하자
2022.07.03 -
[Effective C++] 38. "has-a(..는..를 가짐)" 혹은 "is-implemented-in-terms-of(..는..를 써서 구현됨)"를 모형화할 때는 객체 합성을 사용하자
[Effective C++] 38. "has-a(..는..를 가짐)" 혹은 "is-implemented-in-terms-of(..는..를 써서 구현됨)"를 모형화할 때는 객체 합성을 사용하자
2022.07.03 -
[Effective C++] 37. 어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자
[Effective C++] 37. 어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자
2022.07.02