[Effective C++] #39 private 상속
Scott Meyers의 "Effective C++"를 통해, C++ 구현에 필요한 개념들을 이해하고, 기록하기 위함입니다. 해당 항목은 6장 "상속, 그리고 객체 지향 설계", 항목 39 "private 상속은 심사숙고해서 구사하자"에 해당하는 내용입니다.
private 상속의 특징
class Person {...};
class Student : private Person {...};
void Eat(const Person& p){...};
Person* p;
Student* s;
Eat(p); // OK!
Eat(s); // NO!
private 상속의 의미는 "is-implemented-in-terms-of"입니다. private 상속의 첫 번째 동작 규칙으로 컴파일러는 파생 클래스 객체를 기본 클래스 객체로 변환하지 않습니다. 두 번째 동작 규칙은 기본 클래스의 전체 멤버들 모두 파생 클래스의 private 멤버가 됩니다. 정리하자면, 파생 클래스는 기본 클래스의 몇 가지 기능들을 활용하여 구현되며, 어떠한 개념적 밀접한 관계도 존재하지 않습니다. 따라서, 소프트웨어 설계에 영향을 미치지 못하고, 소프트웨어 구현 중에만 의미를 가집니다.
객체 합성 > private 상속?
// private 상속, "is-implemented-in-terms-of"
class Timer
{
public:
explicit Timer (int tickFrequency);
virtual void LogOnTick() const;
...
};
class Widget : private Timer
{
private:
virtual void LogOnTick() const;
...
};
먼저, 위 예제 코드를 살펴보겠습니다. Widget 클래스는 Timer 클래스를 private 상속받아 해당 클래스에서 벌어지는 일들을 tick count 마다 수집하는 LogOnTick 가상 함수를 상속받습니다. 이 상속 관계는 앞서 말했듯이, "is-implemented-in-terms-of"이며, Widget 클래스는 Timer의 구현만 물려받습니다. 따라서, 사용자는 Widget을 통해 LogOnTick 함수를 호출해선 안 되겠죠(구현만 물려받기 때문에...). 더불어, Timer 클래스의 public 멤버였던 LogOnTick 멤버 함수는 Widget 클래스의 private 멤버가 됩니다.
// 객체 합성, "has-a" + "is-implemented-in-terms-of" 관계
class Widget{
private:
class WidgetTimer : public Timer{
public:
virtual void LogOnTick() const;
...
};
WidgetTimer wTimer;
}
다음 예제 코드를 살펴보겠습니다. 두 번째 예제 코드는 구현 영역의 객체 합성을 이용한 Widget 클래스 구현 방법입니다. Timer 클래스를 public 상속 받는 WidgetTimer 클래스를 private 영역에 중첩 클래스로 선언합니다. 간단하게 말하자면, private 상속 대신에 public 상속 + 객체 합성 조합을 사용합니다. 이 방법은 두 가지 장점을 갖고 있습니다.
1. 파생 클래스 생성 + 가상 함수 재정의 제한
하나는 파생 클래스 생성의 가능성을 열어 두되, LogOnTick 함수의 재정의를 제한할 수 있습니다. 파생 클래스가 Widget 클래스를 private 상속하면 가상 함수를 호출할 수 없더라도, 재정의는 가능합니다. 하지만, WidgetTimer 클래스 안에 LogOnTick 가상 함수를 멤버로 두어, 파생 클래스의 접근을 제한합니다.
2. 컴파일 의존성 최소화
다른 하나는 Widget 클래스의 컴파일 의존성을 감소시킵니다. 만약 Widget 클래스를 Timer 클래스로부터 private 상속 받도록 설계한다면, #include TImer.h 등의 코드 작성이 필요할 수도 있습니다. 반면에, 객체 합성을 사용하여 Widget 클래스가 private 영역에 WidgetTimer 클래스를 가리키는 포인터만 두어 간단하게 구성 요소를 분리할 수 있습니다. 따라서, 컴파일 의존성이 최소화됩니다.
private 상속이 필요한 경우, 독립 구조의 객체
class Empty{};
class SomeClass()
{
private:
int x;
Empty e;
}
위 예제 코드를 살펴보겠습니다. 놀랍게도 SomeClass의 크기는 sizeof(int)보다 큽니다. C++는 "독립 구조의 객체의 크기는 반드시 크기가 0을 넘어야 한다"라고 정의합니다. 따라서, 컴파일러는 공백 객체, 즉 Empty 클래스 객체의 크기가 "0"이 되는 것을 방지하기 위해, "char" 한 개를 끼워 넣는 식으로 처리합니다.
그렇다면, 크기가 "0"이 되어야 마땅한 객체를 "독립 구조"가 아니라, 기본 클래스로 두어 파생 클래스로 하여금 상속하도록 한다면 어떻게 될까요? 다시 말해, 공백 객체를 홀로 두지 않고, 여타 파생 클래스들이 상속하도록 두어 "독립 구조"가 되는 것을 방지하도록 하는 겁니다. 이때, private 상속을 사용합니다.
공백 기본 클래스 최적화
class SomeClass : private Empty
{
private:
int x;
}
위 예제 코드대로 설계한다면, sizeof(SomeClass) == sizeof(int)가 가능해집니다. 이러한 공간 최적화 기법을 "공백 기본 클래스 최적화(EBO)"라고 합니다.
"is-implemented-in-terms-of" 관계는 객체 합성 or private 상속을 표현 가능합니다. 특별한 경우가 아니라면, 객체 합성을 사용합시다.
'언어 > Effective C++' 카테고리의 다른 글
[Effective C++] #41 템플릿, 암시적 인터페이스, 컴파일 다형성, 유효 표현식 (0) | 2022.02.18 |
---|---|
[Effective C++] #40 다중 상속 (0) | 2022.02.17 |
[Effective C++] #38 객체 합성, private 영역, "has-a" ,"is-implemented-in-terms-of" (0) | 2022.02.09 |
[Effective C++] #37 가상 함수, 기본 매개변수 (0) | 2022.02.09 |
[Effective C++] #36 비 가상 함수의 상속, 바인딩 (0) | 2022.02.04 |